initial commit
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
*~
|
||||
#*
|
||||
*#
|
||||
.#*
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
.springBeans
|
||||
target/
|
||||
_site/
|
||||
.idea
|
||||
*.iml
|
||||
*.swp
|
||||
60
.settings.xml
Normal file
60
.settings.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings>
|
||||
<servers>
|
||||
<server>
|
||||
<id>repo.spring.io</id>
|
||||
<username>${env.CI_DEPLOY_USERNAME}</username>
|
||||
<password>${env.CI_DEPLOY_PASSWORD}</password>
|
||||
</server>
|
||||
</servers>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>spring</id>
|
||||
<activation><activeByDefault>true</activeByDefault></activation>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>http://repo.spring.io/libs-snapshot-local</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>http://repo.spring.io/libs-milestone-local</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-releases</id>
|
||||
<name>Spring Releases</name>
|
||||
<url>http://repo.spring.io/release</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>http://repo.spring.io/libs-snapshot-local</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>http://repo.spring.io/libs-milestone-local</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
</settings>
|
||||
19
.travis.yml
Normal file
19
.travis.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
language: java
|
||||
before_install:
|
||||
- git config user.name "$GIT_NAME"
|
||||
- git config user.email "$GIT_EMAIL"
|
||||
- git config credential.helper "store --file=.git/credentials"
|
||||
- echo "https://$GH_TOKEN:@github.com" > .git/credentials
|
||||
- gem install asciidoctor
|
||||
install:
|
||||
- mvn --settings .settings.xml install -P docs -q -U -DskipTests=true -Dmaven.test.redirectTestOutputToFile=true
|
||||
- ./docs/src/main/asciidoc/ghpages.sh
|
||||
script:
|
||||
- '[ "${TRAVIS_PULL_REQUEST}" != "false" ] || mvn --settings .settings.xml deploy -nsu -Dmaven.test.redirectTestOutputToFile=true'
|
||||
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] || mvn --settings .settings.xml install -nsu -Dmaven.test.redirectTestOutputToFile=true'
|
||||
env:
|
||||
global:
|
||||
- GIT_NAME="Dave Syer"
|
||||
- GIT_EMAIL=dsyer@pivotal.io
|
||||
- CI_DEPLOY_USERNAME=buildmaster
|
||||
- secure: aeLXRC5oFSddwnZt1/7G2/OHr7jDbxz0ET7sej3I+eSbe3N5vbzQ6FC08es4l89l54ciXd90I1g2BMw7DTYKOO373FP78XPdAEbifJTU4DGd6fCELmoTtUPhjunBIk7E49hisPbv82892IYYA7qi/hzG548cPyZ1IgiJjq0NCsc=
|
||||
12
README.md
Normal file
12
README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
spring-cloud-consul
|
||||
===================
|
||||
- [X] consul config
|
||||
- [X] consul service discovery
|
||||
- [X] consul ribbon load balancer
|
||||
- [X] consul ui (on the sample agent)
|
||||
- [X] consul property source
|
||||
- [X] consul event bus
|
||||
- [X] send messages
|
||||
- [X] receive messages
|
||||
- [ ] consul locks
|
||||
- [ ] consul leader election
|
||||
748
consul_ui/index.html
Normal file
748
consul_ui/index.html
Normal file
@@ -0,0 +1,748 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7, user-scalable=no">
|
||||
<title>Consul</title>
|
||||
<link rel="stylesheet" href="static/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="static/base.css">
|
||||
<link rel="shortcut icon" href="static/favicon.png">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="container">
|
||||
<div class="col-md-12">
|
||||
<div id="app">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="push"></div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="container">
|
||||
<div class="col-md-12">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/x-handlebars">
|
||||
{{outlet}}
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="error">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2 col-sm-12 col-xs-12">
|
||||
<div class="text-center vertical-center">
|
||||
{{#if controller.model.statusText }}
|
||||
<p class="bold">HTTP error code from Consul: <code>{{controller.model.status}} {{controller.model.statusText}}</code></p>
|
||||
{{/if}}
|
||||
<p>This is an error page for the Consul web UI. You may have visited a URL that is loading an
|
||||
unknown resource, so you can try going back to the <a href="#">root</a>.</p>
|
||||
<p>Otherwise, please report any unexpected
|
||||
issues on the <a href="https://github.com/hashicorp/consul">GitHub page</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="dc/unauthorized">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2 col-sm-12 col-xs-12">
|
||||
<div class="text-center vertical-center">
|
||||
<p class="bold">Access Denied</p>
|
||||
{{#if aclToken}}
|
||||
<p>Your ACL token, <code>{{aclToken}}</code>, does not
|
||||
have the appropriate permissions to perform the expected action.</p>
|
||||
{{else}}
|
||||
<p>The default agent token does not
|
||||
have the appropriate permissions to perform the expected action.</p>
|
||||
{{/if}}
|
||||
<p>Learn more in the <a href="http://www.consul.io/docs/internals/acl.html">ACL documentation</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="dc/aclsdisabled">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2 col-sm-12 col-xs-12">
|
||||
<div class="text-center vertical-center">
|
||||
<p class="bold">ACLs Disabled</p>
|
||||
<p>ACLs are disabled in this Consul cluster. This is the default behavior, as you have to explicitly enable them.</p>
|
||||
</p>Learn more in the <a href="http://www.consul.io/docs/internals/acl.html">ACL documentation</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="loading">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2 col-sm-12 col-xs-12">
|
||||
<div class="text-center vertical-center">
|
||||
<img src="static/loading-cylon-purple.svg" width="384" height="48">
|
||||
<p><small>Loading...</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="actionbar">
|
||||
<div class="row">
|
||||
<div class="action-bar">
|
||||
<div {{ bind-attr class="searchBar:col-md-10:col-md-5" }} >
|
||||
<div class="form-group">
|
||||
{{ input type="text" value=filter class="form-control form-control-mini" placeholder=filterText}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if newAclButton }}
|
||||
<div class="col-md-2">
|
||||
<div class="form-group">
|
||||
{{#link-to 'acls' class='btn btn-mini btn-default btn-noactive pull-right'}}New ACL{{/link-to}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if statuses}}
|
||||
<div class="col-md-5">
|
||||
<div class="form-group">
|
||||
{{view Ember.Select content=statuses value=status class="form-control form-control-mini"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if hasExpanded }}
|
||||
<div class="col-md-2 hidden-xs hidden-sm">
|
||||
<div class="form-group">
|
||||
<button {{ bind-attr class=":btn :btn-mini :pull-right condensed:btn-default:btn-primary" }} {{action toggleCondensed }}>Expand</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="dc">
|
||||
<div class="row">
|
||||
<div {{ bind-attr class=":col-md-12 :col-sm-12 :col-xs-12 :topbar" }}>
|
||||
|
||||
<div class="col-md-1 col-sm-2 col-xs-8 col-sm-offset-0 col-xs-offset-1">
|
||||
<a href="#"><div class="top-brand"></div></a>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-3 col-xs-8 col-sm-offset-0 col-xs-offset-1">
|
||||
{{#link-to 'services' class='btn btn-default col-xs-12'}}Services{{/link-to}}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-sm-3 col-xs-8 col-sm-offset-0 col-xs-offset-1">
|
||||
{{#link-to 'nodes' class='btn btn-default col-xs-12'}}Nodes{{/link-to}}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-sm-3 col-xs-8 col-sm-offset-0 col-xs-offset-1">
|
||||
{{#link-to 'kv' class='btn btn-default col-xs-12'}}Key/Value{{/link-to}}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-sm-2 col-xs-8 col-md-offset-0 col-sm-offset-2 col-xs-offset-1">
|
||||
{{#link-to 'acls' class='btn btn-default col-xs-12'}}ACL{{/link-to}}
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-sm-2 col-xs-6 col-md-offset-0 col-sm-offset-4 col-xs-offset-1">
|
||||
<a {{bind-attr class=":col-xs-12 :btn hasFailingChecks:btn-warning:btn-success"}} {{action "toggle"}}> <span class="elip-overflow">{{model}} <span class="caret"></span></span> </a>
|
||||
|
||||
{{#if isDropdownVisible}}
|
||||
<ul class="dropdown-menu col-xs-8" style="display:block;">
|
||||
{{#each dc in dcs}}
|
||||
<li {{action "hideDrop"}}>{{#link-to 'services' dc}}{{dc}}{{/link-to}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-1 col-sm-2 col-xs-2 col-md-offset-0 col-sm-offset-0 col-xs-offset-0">
|
||||
{{#link-to 'settings' class='btn btn-default col-xs-6 icon'}}<span class="wrap">⚙</span>{{/link-to}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{outlet}}
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="kv/show">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-lg-5 padded-right-middle">
|
||||
<h4 class="breadcrumbs"><a href="" {{action 'linkToKey' grandParentKey }}>{{parentKey}}</a> <a href="" {{action 'linkToKey' parentKey }}>+</a></h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-lg-5 padded-right-middle">
|
||||
|
||||
{{#each item in model }}
|
||||
{{#link-to item.linkToRoute item.Key tagName="div" href=false class="list-group-item list-condensed-link" }}
|
||||
<div {{bind-attr class="item.isFolder:bg-gray:bg-light-gray :list-bar-horizontal"}}></div>
|
||||
<div class="name">
|
||||
{{item.keyWithoutParent}}
|
||||
</div>
|
||||
{{/link-to}}
|
||||
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="border-left hidden-xs hidden-sm">
|
||||
</div>
|
||||
|
||||
<div class="visible-xs visible-sm">
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-7 border-left">
|
||||
<div class="padded-border">
|
||||
<div class="panel">
|
||||
<div {{ bind-attr class=":panel-bar isLoading:bg-orange:bg-light-gray" }}></div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
Create Key
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="panel-body panel-form">
|
||||
<form class="form">
|
||||
|
||||
<div class="form-group">
|
||||
<p>{{errorMessage}}</p>
|
||||
</div>
|
||||
|
||||
<div {{ bind-attr class=":form-group newKey.keyValid:valid" }}>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">{{parentKey}}</span>
|
||||
{{ input value=newKey.Key class="form-control" required=true }}
|
||||
</div>
|
||||
<span class="help-block">To create a folder, end the key with <code>/</code></span>
|
||||
</div>
|
||||
|
||||
{{#if newKey.isFolder }}
|
||||
<p>No value needed for nested keys.</p>
|
||||
{{else}}
|
||||
<div class="form-group">
|
||||
{{ textarea value=newKey.Value class="form-control"}}
|
||||
<span class="help-block">Value can be any format and length</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<button {{ action "createKey"}} {{bind-attr disabled=newKey.isInvalid }} {{ bind-attr class=":btn newKey.isValid:btn-success:btn-default" }}>Create</button>
|
||||
<button {{ action "deleteFolder"}} {{ bind-attr class=":btn :pull-right isLoading:btn-warning:btn-danger isRoot:hidden" }}>Delete folder</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="kv/edit">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-lg-5 padded-right-middle">
|
||||
<h4 class="breadcrumbs"><a href="" {{action 'linkToKey' grandParentKey }}>{{parentKey}}</a> <a href="" {{action 'linkToKey' parentKey }}>+</a></h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-lg-5 padded-right-middle">
|
||||
{{#each item in siblings }}
|
||||
{{#link-to item.linkToRoute item.Key tagName="div" href=false class="list-group-item list-condensed-link" }}
|
||||
<div {{bind-attr class="item.isFolder:bg-gray:bg-light-gray :list-bar-horizontal"}}></div>
|
||||
<div class="name">
|
||||
{{item.keyWithoutParent}}
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="border-left hidden-xs hidden-sm">
|
||||
</div>
|
||||
|
||||
<div class="visible-xs visible-sm">
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-7 border-left">
|
||||
<div class="padded-border">
|
||||
|
||||
<div class="panel">
|
||||
<div {{ bind-attr class=":panel-bar isLoading:bg-orange:bg-green isLocked:bg-light-gray" }}></div>
|
||||
<div class="panel-heading">
|
||||
<h4 {{bind-attr class=":panel-title isLocked:locked"}}>
|
||||
{{model.Key}}
|
||||
{{#if model.isLocked}}
|
||||
<small class="pull-right">
|
||||
KEY LOCKED
|
||||
</small>
|
||||
{{/if}}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="panel-body panel-form">
|
||||
<div class="form-group">
|
||||
{{errorMessage}}
|
||||
</div>
|
||||
<form class="form">
|
||||
<div class="form-group">
|
||||
{{ textarea value=model.valueDecoded class="form-control" disabled=model.isLocked}}
|
||||
</div>
|
||||
<button {{action "updateKey"}} {{bind-attr disabled=isLoading}} {{bind-attr class=":btn isLoading:btn-warning:btn-success"}} {{bind-attr disabled=isLocked}}>Update</button>
|
||||
<button {{action "cancelEdit"}} {{bind-attr disabled=isLoading}} {{bind-attr class=":btn isLoading:btn-warning:btn-default"}}>Cancel</button>
|
||||
<button {{action "deleteKey"}} {{bind-attr disabled=isLoading}} {{bind-attr class=":btn :pull-right isLoading:btn-warning:btn-danger"}} {{bind-attr disabled=isLocked}}>Delete key</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if model.isLocked}}
|
||||
<h5>Lock Session</h5>
|
||||
|
||||
{{#link-to 'nodes.show' model.session.Node tagName="div" href=false class="list-group-item list-condensed-link" }}
|
||||
<div class="bg-light-gray list-bar-horizontal"></div>
|
||||
<div class="name">
|
||||
{{ sessionName session }}
|
||||
<small class="pull-right">
|
||||
{{session.Node}}
|
||||
</small>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="item/loading">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2 col-sm-12 col-xs-12">
|
||||
<div class="text-center vertical-center">
|
||||
<img src="static/loading-cylon-purple.svg" width="384" height="48">
|
||||
<p><small>Loading...</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="services">
|
||||
<div class="row">
|
||||
<div {{ bind-attr class=":col-md-6 :col-lg-5 :padded-right-middle isShowingItem:hidden-xs isShowingItem:hidden-sm" }}>
|
||||
{{view App.ActionBarView }}
|
||||
|
||||
{{#if filteredContent}}
|
||||
|
||||
{{#if condensed }}
|
||||
|
||||
{{#collection Ember.ListView contentBinding="filteredContent" height=800 rowHeight=44 }}
|
||||
{{#link-to 'services.show' Name tagName="div" href=false class="list-group-item list-condensed-link" }}
|
||||
<div {{bind-attr class="hasFailingChecks:bg-orange:bg-green :list-bar-horizontal"}}></div>
|
||||
<div class="name">
|
||||
{{Name}}
|
||||
<small class="pull-right">
|
||||
{{ checkMessage }}
|
||||
</small>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/collection}}
|
||||
|
||||
{{else}}
|
||||
|
||||
{{#collection Ember.ListView contentBinding="filteredContent" height=800 rowHeight=120 }}
|
||||
{{#link-to 'services.show' Name tagName="div" href=false class="list-group-item list-link" }}
|
||||
<div {{bind-attr class="hasFailingChecks:bg-orange:bg-green :list-bar"}}></div>
|
||||
<h4 class="list-group-item-heading">
|
||||
{{#link-to 'services.show' Name class='subtle'}}{{Name}}{{/link-to}}
|
||||
<div class="heading-helper">
|
||||
<a class="subtle" href="#">{{checkMessage}}</a>
|
||||
</div>
|
||||
</h4>
|
||||
<ul class="list-inline">
|
||||
{{#each node in nodes }}
|
||||
<li class="bold">{{node}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/link-to}}
|
||||
{{/collection}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
{{else}}
|
||||
<p class="light">There are no services to show.</p>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="border-left hidden-xs hidden-sm">
|
||||
</div>
|
||||
|
||||
<div class="visible-xs visible-sm">
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-7 border-left scrollable">
|
||||
<div class="row padded-border">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="service">
|
||||
<div class="col-xs-12 col-sm-12 visible-xs visible-sm">
|
||||
{{#link-to "services" class="btn btn-default btn-block" }}Back to all services{{/link-to}}
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<h3 class="no-margin">{{ model.0.Service.Service }}</h3>
|
||||
<hr>
|
||||
|
||||
<h5>Tags</h5>
|
||||
{{#if tags}}
|
||||
<p>{{tags}}</p>
|
||||
{{else}}
|
||||
<p>No tags</p>
|
||||
{{/if}}
|
||||
|
||||
<h5>Nodes</h5>
|
||||
|
||||
{{#each node in model }}
|
||||
|
||||
{{#link-to 'nodes.show' node.Node.Node tagName="div" href=false class="panel panel-link panel-list" }}
|
||||
<div {{bind-attr class="hasFailingChecks:bg-orange:bg-green :panel-bar-horizontal"}}></div>
|
||||
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{node.Node.Node}}
|
||||
<small>{{node.Node.Address}}</small>
|
||||
<span class="panel-note">{{node.checkMessage}}</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ul class="list-unstyled">
|
||||
{{#each check in node.Checks }}
|
||||
<li class="list-group-item list-condensed-link">
|
||||
{{listBar check.Status}}
|
||||
<div class="name">
|
||||
{{check.Name}}
|
||||
<small>{{ check.CheckID }}</small>
|
||||
<small class="pull-right">
|
||||
{{check.Status}}
|
||||
</small>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
{{/link-to}}
|
||||
|
||||
{{/each}}
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="nodes">
|
||||
<div class="row">
|
||||
<div {{ bind-attr class=":col-md-6 :col-lg-5 :padded-right-middle isShowingItem:hidden-xs isShowingItem:hidden-sm" }}>
|
||||
{{view App.ActionBarView }}
|
||||
|
||||
{{#if filteredContent}}
|
||||
|
||||
{{#if condensed }}
|
||||
{{#collection Ember.ListView contentBinding="filteredContent" height=800 rowHeight=44 }}
|
||||
{{#link-to 'nodes.show' Node tagName="div" href=false class="list-group-item list-condensed-link" }}
|
||||
<div {{bind-attr class="hasFailingChecks:bg-orange:bg-green :list-bar-horizontal"}}></div>
|
||||
<div class="name">
|
||||
{{Node}}
|
||||
<small class="pull-right">
|
||||
{{ numServices }} services
|
||||
</small>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/collection}}
|
||||
|
||||
{{else}}
|
||||
|
||||
{{#collection Ember.ListView contentBinding="filteredContent" height=800 rowHeight=120 }}
|
||||
{{#link-to 'nodes.show' Node tagName="div" href=false class="list-group-item list-link" }}
|
||||
<div {{bind-attr class="hasFailingChecks:bg-orange:bg-green :list-bar"}}></div>
|
||||
<h4 class="list-group-item-heading">
|
||||
{{Node}}
|
||||
<small>{{Address}}</small>
|
||||
<div class="heading-helper">
|
||||
<a class="subtle" href="#">{{checkMessage}}</a>
|
||||
</div>
|
||||
</h4>
|
||||
<ul class="list-inline">
|
||||
{{#each service in services}}
|
||||
<li class="bold">{{service.Service}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/link-to}}
|
||||
{{/collection}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
{{else}}
|
||||
<p class="light">There are no nodes to show.</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="border-left hidden-xs hidden-sm">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-7 border-left scrollable">
|
||||
<div class="row padded-border">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="node">
|
||||
<div class="col-xs-12 col-sm-12 visible-xs visible-sm">
|
||||
{{#link-to "nodes" class="btn btn-default btn-block" }}Back to all nodes{{/link-to}}
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<h3 class="no-margin">
|
||||
{{ model.Node }} <small> {{ model.Address }}</small>
|
||||
<button {{ action "deregisterNode" }} {{ bind-attr class=":btn :btn-danger :pull-right :btn-mini isLoading:btn-warning" }}>Deregister</button>
|
||||
</h3>
|
||||
<hr>
|
||||
|
||||
<h5>Services</h5>
|
||||
|
||||
{{#each service in model.Services }}
|
||||
{{#link-to 'services.show' service.Service tagName="div" href=false class="list-group-item list-condensed-link double-line" }}
|
||||
<div class="list-bar-horizontal bg-light-gray"></div>
|
||||
<div class="name">
|
||||
{{service.Service}}
|
||||
<small class="pull-right">
|
||||
:{{service.Port}}
|
||||
</small>
|
||||
</div>
|
||||
<ul class="list-inline sub">
|
||||
{{#each tag in service.Tags}}
|
||||
<li>{{tag}}</li>
|
||||
{{/each}}
|
||||
{{serviceTagMessage service.Tags}}
|
||||
</ul>
|
||||
{{/link-to}}
|
||||
|
||||
{{/each}}
|
||||
|
||||
<h5>Checks</h5>
|
||||
|
||||
{{#each check in model.Checks }}
|
||||
|
||||
<div class="panel">
|
||||
{{ panelBar check.Status }}
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{check.Name}}
|
||||
<small>{{check.CheckID}}</small>
|
||||
<span class="panel-note">{{check.Status}}</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<h5>Notes</h5>
|
||||
<p>{{ check.Notes }}</p>
|
||||
<h5>Output</h5>
|
||||
<pre>{{check.Output}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/each}}
|
||||
|
||||
<h5>Lock Sessions</h5>
|
||||
|
||||
{{#if sessions }}
|
||||
{{errorMessage}}
|
||||
|
||||
{{#each session in sessions }}
|
||||
<div class="list-group-item list-condensed double-line">
|
||||
<div class="bg-light-gray list-bar-horizontal"></div>
|
||||
<div class="name">
|
||||
{{ sessionName session }}
|
||||
<button {{ action "invalidateSession" session.ID }} {{ bind-attr class=":btn :btn-danger :pull-right :btn-list isLoading:btn-warning" }}>Invalidate</button>
|
||||
</div>
|
||||
<ul class="list-inline sub">
|
||||
{{#each check in session.Checks}}
|
||||
<li class="bold">{{check}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{else}}
|
||||
<p class="light small">No sessions</p>
|
||||
{{/if}}
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="acls">
|
||||
<div class="row">
|
||||
<div {{ bind-attr class=":col-md-6 :col-lg-5 :padded-right-middle isShowingItem:hidden-xs isShowingItem:hidden-sm" }}>
|
||||
{{view App.ActionBarView }}
|
||||
|
||||
{{#if filteredContent}}
|
||||
{{#collection Ember.ListView contentBinding="filteredContent" height=800 rowHeight=44 }}
|
||||
{{#link-to 'acls.show' ID tagName="div" href=false class="list-group-item list-condensed-link" }}
|
||||
<div class="bg-light-gray list-bar-horizontal"></div>
|
||||
<div class="name">
|
||||
{{ aclName Name ID }}
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/collection}}
|
||||
{{else}}
|
||||
<p class="light">There are no ACLs to show.</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="border-left hidden-xs hidden-sm">
|
||||
</div>
|
||||
|
||||
{{#if isShowingItem}}
|
||||
<div class="col-md-6 col-lg-7 border-left scrollable">
|
||||
<div class="row padded-border">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="col-md-6 col-lg-7 border-left">
|
||||
<div class="row padded-border">
|
||||
<div class="panel">
|
||||
<div {{ bind-attr class=":panel-bar isLoading:bg-orange:bg-light-gray" }}></div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
New ACL
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="panel-body panel-form">
|
||||
<div class="form-group"></div>
|
||||
|
||||
<form class="form">
|
||||
<div class="form-group">
|
||||
{{ input value=newAcl.Name class="form-control" }}
|
||||
<span class="help-block">Set the optional name for the ACL.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{view Ember.Select content=types value=newAcl.Type class="form-control form-control-mini"}}
|
||||
<span class="help-block">The type of ACL this is.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Rules</label>
|
||||
{{ textarea value=newAcl.Rules class="form-control" }}
|
||||
<span class="help-block">For more information on rules, visit the <a href="http://www.consul.io/docs/internals/acl.html">ACL documentation.</a></span>
|
||||
</div>
|
||||
|
||||
<button {{ action "createAcl"}} {{ bind-attr class=":btn :btn-success" }}>Create</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="acl">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-12 visible-xs visible-sm">
|
||||
{{#link-to "acls" class="btn btn-default btn-block" }}Back to all ACLs{{/link-to}}
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div {{ bind-attr class=":panel-bar isLoading:bg-orange:bg-light-gray" }}></div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
{{ aclName "Update ACL" model.ID }}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="panel-body panel-form">
|
||||
<div class="form-group"></div>
|
||||
|
||||
<form class="form">
|
||||
<div class="form-group">
|
||||
{{ input value=model.Name class="form-control" }}
|
||||
<span class="help-block">Set the optional name for the ACL.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{view Ember.Select content=types value=model.Type class="form-control form-control-mini"}}
|
||||
<span class="help-block">The type of ACL this is.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Rules</label>
|
||||
{{ textarea value=model.Rules class="form-control" }}
|
||||
<span class="help-block">For more information on rules, visit the <a href="http://www.consul.io/docs/internals/acl.html">ACL documentation.</a></span>
|
||||
</div>
|
||||
|
||||
<button {{ action "updateAcl"}} {{ bind-attr class=":btn :btn-success" }}>Update</button>
|
||||
<button {{ action "clone" }} {{ bind-attr class=":btn :btn-default" }}>Clone</button>
|
||||
<button {{ action "set" }} {{ bind-attr class=":btn :btn-default" }}>Use Token</button>
|
||||
{{# if model.isNotAnon }}
|
||||
<button {{ action "delete"}} {{ bind-attr class=":btn isLoading:btn-warning:btn-danger :pull-right" }}>Delete</button>
|
||||
{{/if}}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="index">
|
||||
<div class="col-md-8 col-md-offset-2 col-xs-offset-0 col-sm-offset-0 col-xs-12 col-sm-12 vertical-center">
|
||||
<h5>Select a datacenter</h5>
|
||||
{{#each item in model}}
|
||||
{{#link-to 'services' item }}
|
||||
<div class="panel panel-link panel-short">
|
||||
<div class="panel-bar bg-light-gray"></div>
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{{item}}
|
||||
<span class="panel-note"></span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" id="settings">
|
||||
<div class="col-md-8 col-md-offset-2 col-xs-offset-0 col-sm-offset-0 col-xs-12 col-sm-12">
|
||||
<h3>Settings</h3>
|
||||
<p>These settings allow you to configure your browser for the Consul Web UI. Everything is saved to localstorage,
|
||||
and should persist through visits and browser usage.</p>
|
||||
<p>Settings are automatically persisted upon modification, so no manual save is required.</p>
|
||||
<h5>Access Token</h5>
|
||||
<div class="form-group">
|
||||
{{ input type="text" value=model.token class="form-control form-mono" placeholder="token"}}
|
||||
<span class="help-block">The token is sent with requests as the <code>?token</code> parameter. This is used to control the ACL for the
|
||||
web UI.</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button {{ action "reset" }} {{ bind-attr class=":btn :btn-danger" }}>Reset Defaults</button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
||||
<script>
|
||||
// Enable query params, must be loaded before ember is
|
||||
EmberENV = {FEATURES: {'query-params-new': true}};
|
||||
</script>
|
||||
|
||||
|
||||
<script src="static/application.min.js"></script></body>
|
||||
</html>
|
||||
23010
consul_ui/static/application.min.js
vendored
Normal file
23010
consul_ui/static/application.min.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1118
consul_ui/static/base.css
Normal file
1118
consul_ui/static/base.css
Normal file
File diff suppressed because it is too large
Load Diff
7
consul_ui/static/bootstrap.min.css
vendored
Normal file
7
consul_ui/static/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
consul_ui/static/consul-logo.png
Normal file
BIN
consul_ui/static/consul-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
consul_ui/static/favicon.png
Normal file
BIN
consul_ui/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
11
consul_ui/static/loading-cylon-purple.svg
Normal file
11
consul_ui/static/loading-cylon-purple.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 14 32 18" width="32" height="4" fill="#9e84c5" preserveAspectRatio="none">
|
||||
<path opacity="0.8" transform="translate(0 0)" d="M2 14 V18 H6 V14z">
|
||||
<animateTransform attributeName="transform" type="translate" values="0 0; 24 0; 0 0" dur="2s" begin="0" repeatCount="indefinite" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
|
||||
</path>
|
||||
<path opacity="0.5" transform="translate(0 0)" d="M0 14 V18 H8 V14z">
|
||||
<animateTransform attributeName="transform" type="translate" values="0 0; 24 0; 0 0" dur="2s" begin="0.1s" repeatCount="indefinite" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
|
||||
</path>
|
||||
<path opacity="0.25" transform="translate(0 0)" d="M0 14 V18 H8 V14z">
|
||||
<animateTransform attributeName="transform" type="translate" values="0 0; 24 0; 0 0" dur="2s" begin="0.2s" repeatCount="indefinite" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" calcMode="spline" />
|
||||
</path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 983 B |
148
pom.xml
Normal file
148
pom.xml
Normal file
@@ -0,0 +1,148 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Spring Cloud Consul</name>
|
||||
<description>Spring Cloud Consul</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-build</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<relativePath/>
|
||||
<!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/spring-cloud/spring-cloud-consul</url>
|
||||
<connection>scm:git:git://github.com/spring-cloud/spring-cloud-consul.git</connection>
|
||||
<developerConnection>scm:git:ssh://git@github.com/spring-cloud/spring-cloud-consul.git</developerConnection>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
|
||||
<modules>
|
||||
<module>spring-cloud-consul-core</module>
|
||||
<module>spring-cloud-consul-config</module>
|
||||
<module>spring-cloud-consul-discovery</module>
|
||||
<module>spring-cloud-consul-bus</module>
|
||||
<module>spring-cloud-consul-sample</module>
|
||||
</modules>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.1</version>
|
||||
<configuration>
|
||||
<source>1.7</source>
|
||||
<target>1.7</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-commons</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul-core</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul-bus</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul-config</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul-discovery</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-bus</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-amqp</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.integration</groupId>
|
||||
<artifactId>spring-integration-amqp</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-spring-service-connector</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-localconfig-connector</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-cloudfoundry-connector</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-client</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.feign</groupId>
|
||||
<artifactId>feign-core</artifactId>
|
||||
<version>${feign.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.feign</groupId>
|
||||
<artifactId>feign-jackson</artifactId>
|
||||
<version>${feign.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.ribbon</groupId>
|
||||
<artifactId>ribbon</artifactId>
|
||||
<version>${ribbon.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.ribbon</groupId>
|
||||
<artifactId>ribbon-core</artifactId>
|
||||
<version>${ribbon.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.12.6</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>18.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<properties>
|
||||
<feign.version>6.1.2</feign.version>
|
||||
<ribbon.version>2.0-RC9</ribbon.version>
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
2
run_consul.sh
Executable file
2
run_consul.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -ui-dir `dirname $0`/consul_ui
|
||||
45
spring-cloud-consul-bus/pom.xml
Normal file
45
spring-cloud-consul-bus/pom.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-consul-bus</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring Cloud Consul Bus</name>
|
||||
<description>Spring Cloud Consul Bus</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-bus</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<!-- Only needed at compile time -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.springframework.cloud.consul.bus;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.cloud.bus.BusAutoConfiguration;
|
||||
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
|
||||
import org.springframework.cloud.consul.client.EventClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.integration.dsl.IntegrationFlow;
|
||||
import org.springframework.integration.dsl.IntegrationFlows;
|
||||
import org.springframework.integration.dsl.support.Transformers;
|
||||
import org.springframework.integration.support.json.Jackson2JsonObjectMapper;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(EventClient.class)
|
||||
@ConditionalOnExpression("${bus.consul.enabled:true}")
|
||||
@AutoConfigureAfter(BusAutoConfiguration.class)
|
||||
@EnableScheduling
|
||||
public class ConsulBusAutoConfiguration {
|
||||
@Autowired
|
||||
@Qualifier("cloudBusInboundChannel") MessageChannel cloudBusInboundChannel;
|
||||
|
||||
@Autowired
|
||||
ObjectMapper objectMapper;
|
||||
|
||||
@Bean
|
||||
public ConsulOutboundEndpoint consulOutboundEndpoint() {
|
||||
return new ConsulOutboundEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IntegrationFlow cloudBusConsulOutboundFlow(
|
||||
@Qualifier("cloudBusOutboundChannel") MessageChannel cloudBusOutboundChannel) {
|
||||
return IntegrationFlows
|
||||
.from(cloudBusOutboundChannel)
|
||||
//TODO: put the json headers as part of the message, here?
|
||||
.transform(Transformers.toJson())
|
||||
.handle(consulOutboundEndpoint())
|
||||
.get();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IntegrationFlow cloudBusConsulInboundFlow() {
|
||||
return IntegrationFlows
|
||||
.from(consulInboundChannelAdapter())
|
||||
.transform(Transformers.fromJson(RemoteApplicationEvent.class, new Jackson2JsonObjectMapper(objectMapper)))
|
||||
.channel(cloudBusInboundChannel) // now set in consulInboundChannelAdapter bean
|
||||
.get();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConsulInboundChannelAdapter consulInboundChannelAdapter() {
|
||||
ConsulInboundChannelAdapter adapter = new ConsulInboundChannelAdapter();
|
||||
adapter.setOutputChannel(cloudBusInboundChannel);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.springframework.cloud.consul.bus;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.consul.client.EventService;
|
||||
import org.springframework.cloud.consul.model.Event;
|
||||
import org.springframework.integration.endpoint.MessageProducerSupport;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Adapter that receives Messages from Consul Events, converts them into
|
||||
* Spring Integration Messages, and sends the results to a Message Channel.
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulInboundChannelAdapter extends MessageProducerSupport {
|
||||
@Autowired
|
||||
private EventService eventService;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
public ConsulInboundChannelAdapter() {
|
||||
}
|
||||
|
||||
//link eventService to sendMessage
|
||||
/*
|
||||
Map<String, Object> headers = headerMapper.toHeadersFromRequest(message.getMessageProperties());
|
||||
if (messageListenerContainer.getAcknowledgeMode() == AcknowledgeMode.MANUAL) {
|
||||
headers.put(AmqpHeaders.DELIVERY_TAG, message.getMessageProperties().getDeliveryTag());
|
||||
headers.put(AmqpHeaders.CHANNEL, channel);
|
||||
}
|
||||
sendMessage(AmqpInboundChannelAdapter.this.getMessageBuilderFactory().withPayload(payload).copyHeaders(headers).build());*/
|
||||
|
||||
//start thread
|
||||
//make blocking calls
|
||||
//foreach event -> send message
|
||||
|
||||
|
||||
@Override
|
||||
protected void doStart() {
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "10")
|
||||
public void getEvents() throws IOException {
|
||||
List<Event> events = eventService.watch();
|
||||
for (Event event : events) {
|
||||
Map<String, Object> headers = new HashMap<>();
|
||||
//headers.put(MessageHeaders.REPLY_CHANNEL, outputChannel.)
|
||||
sendMessage(getMessageBuilderFactory()
|
||||
//TODO: deal with odd objectMapper thing
|
||||
.withPayload(objectMapper.readValue(event.getDecoded(), String.class))
|
||||
//TODO: support headers
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStop() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.springframework.cloud.consul.bus;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.consul.client.EventService;
|
||||
import org.springframework.cloud.consul.model.Event;
|
||||
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* Adapter that converts and sends Messages as Consul events
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulOutboundEndpoint extends AbstractReplyProducingMessageHandler {
|
||||
|
||||
@Autowired
|
||||
protected EventService eventService;
|
||||
|
||||
@Override
|
||||
protected Object handleRequestMessage(Message<?> requestMessage) {
|
||||
Object payload = requestMessage.getPayload();
|
||||
//TODO: support headers
|
||||
//TODO: support consul event filters: NodeFilter, ServiceFilter, TagFilter
|
||||
Event event = eventService.fire("springCloudBus", (String) payload);
|
||||
//TODO: return event?
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.springframework.cloud.consul.bus;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.Data;
|
||||
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@JsonTypeName("simple")
|
||||
@Data
|
||||
public class SimpleRemoteEvent extends RemoteApplicationEvent {
|
||||
|
||||
private String message;
|
||||
|
||||
private SimpleRemoteEvent(){}
|
||||
|
||||
public SimpleRemoteEvent(Object source, String originService, String destinationService, String message) {
|
||||
super(source, originService, destinationService);
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public SimpleRemoteEvent(Object source, String originService, String message) {
|
||||
super(source, originService);
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Auto Configuration
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.consul.bus.ConsulBusAutoConfiguration
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.springframework.cloud.consul.bus;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.Data;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.bus.BusAutoConfiguration;
|
||||
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
|
||||
import org.springframework.cloud.bus.jackson.SubtypeModule;
|
||||
import org.springframework.cloud.consul.ConsulAutoConfiguration;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.integration.config.EnableIntegration;
|
||||
import org.springframework.integration.dsl.support.Transformers;
|
||||
import org.springframework.integration.json.JsonToObjectTransformer;
|
||||
import org.springframework.integration.support.json.Jackson2JsonObjectMapper;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
public class ConsulBusIT {
|
||||
|
||||
@Test
|
||||
public void test001ConsulOutboundEndpoint_HandleRequestMessage() {
|
||||
ConfigurableApplicationContext context = getOutboundContext();
|
||||
context.publishEvent(new SimpleRemoteEvent(this, "testService", "testMessage"));
|
||||
}
|
||||
|
||||
private ConfigurableApplicationContext getOutboundContext() {
|
||||
System.setProperty("spring.cloud.config.enabled", "false");
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder()
|
||||
.web(false)
|
||||
.sources(OutboundConfig.class)
|
||||
.run();
|
||||
context.setId("testService");
|
||||
return context;
|
||||
}
|
||||
|
||||
/*@Test
|
||||
public void test002ConsulInboundChannelAdapter() {
|
||||
ConfigurableApplicationContext inbound = getInboundContext();
|
||||
ConfigurableApplicationContext outbound = getOutboundContext();
|
||||
outbound.publishEvent(new TestMessage(this, "testService", "inboundTestService", "testMessage"));
|
||||
|
||||
InboundConfig inboundConfig = inbound.getBean(InboundConfig.class);
|
||||
assertNotNull("message was null", inboundConfig.message);
|
||||
}
|
||||
|
||||
private ConfigurableApplicationContext getInboundContext() {
|
||||
System.setProperty("spring.cloud.config.enabled", "false");
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder()
|
||||
.web(false)
|
||||
.sources(InboundConfig.class)
|
||||
.run();
|
||||
context.setId("inboundTestService");
|
||||
return context;
|
||||
}*/
|
||||
|
||||
protected static final String JSON_PAYLOAD = "{\"type\":\"simple\",\"timestamp\":1416349427372,\"originService\":\"testService\",\"destinationService\":null,\"headers\":{},\"message\":\"testMessage\"}";
|
||||
|
||||
@Test
|
||||
public void test003JsonToObject() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new SubtypeModule(SimpleRemoteEvent.class));
|
||||
JsonToObjectTransformer transformer = Transformers.fromJson(RemoteApplicationEvent.class, new Jackson2JsonObjectMapper(objectMapper));
|
||||
/*HashMap<String, Object> map = new HashMap<>();
|
||||
map.put(JsonHeaders.TYPE_ID, RemoteApplicationEvent.class);*/
|
||||
Message<?> message = transformer.transform(new GenericMessage<String>(JSON_PAYLOAD/*, map*/));
|
||||
Object payload = message.getPayload();
|
||||
assertTrue("payload is of wrong type", payload instanceof RemoteApplicationEvent);
|
||||
assertTrue("payload is of wrong type", payload instanceof SimpleRemoteEvent);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import({ConsulAutoConfiguration.class, BusAutoConfiguration.class, ConsulBusAutoConfiguration.class})
|
||||
@EnableIntegration
|
||||
public static class OutboundConfig {
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new SubtypeModule(SimpleRemoteEvent.class));
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import({ConsulAutoConfiguration.class, BusAutoConfiguration.class, ConsulBusAutoConfiguration.class})
|
||||
@EnableIntegration
|
||||
public static class InboundConfig implements ApplicationListener<RemoteApplicationEvent> {
|
||||
RemoteApplicationEvent message;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(RemoteApplicationEvent event) {
|
||||
this.message = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
spring-cloud-consul-config/pom.xml
Normal file
45
spring-cloud-consul-config/pom.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-consul-config</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring Cloud Consul Config</name>
|
||||
<description>Spring Cloud Consul Config</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<!-- Only needed at compile time -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.springframework.cloud.consul.config;
|
||||
|
||||
import org.springframework.cloud.consul.ConsulAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration
|
||||
@Import(ConsulAutoConfiguration.class)
|
||||
public class ConsulConfigBootstrapConfiguration {
|
||||
@Bean
|
||||
public ConsulPropertySourceLocator consulPropertySourceLocator() {
|
||||
return new ConsulPropertySourceLocator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.springframework.cloud.consul.config;
|
||||
|
||||
import org.springframework.cloud.consul.client.KeyValueClient;
|
||||
import org.springframework.cloud.consul.client.NotFoundException;
|
||||
import org.springframework.cloud.consul.model.KeyValue;
|
||||
import org.springframework.core.env.EnumerablePropertySource;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulPropertySource extends EnumerablePropertySource<KeyValueClient> {
|
||||
|
||||
private String context;
|
||||
|
||||
private Map<String, String> properties = new LinkedHashMap<>();
|
||||
|
||||
public ConsulPropertySource(String context, KeyValueClient source) {
|
||||
super(context, source);
|
||||
this.context = context;
|
||||
|
||||
if (!this.context.endsWith("/")) {
|
||||
this.context = this.context + "/";
|
||||
}
|
||||
}
|
||||
|
||||
public void init() {
|
||||
try {
|
||||
List<KeyValue> keyValues = source.getKeyValueRecurse(context);
|
||||
|
||||
for (KeyValue keyValue : keyValues) {
|
||||
String key = keyValue.getKey()
|
||||
.replace(context, "")
|
||||
.replace('/', '.');
|
||||
String value = keyValue.getDecoded();
|
||||
properties.put(key, value);
|
||||
}
|
||||
} catch (NotFoundException e) {
|
||||
//not found, do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(String name) {
|
||||
return properties.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getPropertyNames() {
|
||||
return properties.keySet().toArray(new String[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.springframework.cloud.consul.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.config.client.PropertySourceLocator;
|
||||
import org.springframework.cloud.consul.ConsulProperties;
|
||||
import org.springframework.cloud.consul.client.KeyValueClient;
|
||||
import org.springframework.core.env.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulPropertySourceLocator implements PropertySourceLocator {
|
||||
|
||||
@Autowired
|
||||
private KeyValueClient keyValueClient;
|
||||
|
||||
@Autowired
|
||||
private ConsulProperties properties;
|
||||
|
||||
@Override
|
||||
public PropertySource<?> locate(Environment environment) {
|
||||
if (environment instanceof ConfigurableEnvironment) {
|
||||
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
|
||||
String appName = env.getProperty("spring.application.name");
|
||||
List<String> profiles = Arrays.asList(env.getActiveProfiles());
|
||||
|
||||
String prefix = properties.getPrefix();
|
||||
List<String> contexts = new ArrayList<>();
|
||||
|
||||
String defaultContext = prefix + "/application";
|
||||
contexts.add(defaultContext + "/");
|
||||
addProfiles(contexts, defaultContext, profiles);
|
||||
|
||||
String baseContext = prefix + "/" + appName;
|
||||
contexts.add(baseContext + "/");
|
||||
addProfiles(contexts, baseContext, profiles);
|
||||
|
||||
CompositePropertySource composite = new CompositePropertySource("consul");
|
||||
|
||||
for (String propertySourceContext : contexts) {
|
||||
ConsulPropertySource propertySource = create(propertySourceContext);
|
||||
propertySource.init();
|
||||
composite.addPropertySource(propertySource);
|
||||
}
|
||||
|
||||
return composite;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ConsulPropertySource create(String context) {
|
||||
return new ConsulPropertySource(context, keyValueClient);
|
||||
}
|
||||
|
||||
private void addProfiles(List<String> contexts, String baseContext, List<String> profiles) {
|
||||
for (String profile : profiles) {
|
||||
contexts.add(baseContext + "::" + profile + "/");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Bootstrap Configuration
|
||||
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
|
||||
org.springframework.cloud.consul.config.ConsulConfigBootstrapConfiguration
|
||||
57
spring-cloud-consul-core/pom.xml
Normal file
57
spring-cloud-consul-core/pom.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-consul-core</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring Cloud Consul Core</name>
|
||||
<description>Spring Cloud Consul Core</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-commons</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.feign</groupId>
|
||||
<artifactId>feign-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.feign</groupId>
|
||||
<artifactId>feign-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<!-- Only needed at compile time -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.springframework.cloud.consul;
|
||||
|
||||
import feign.Feign;
|
||||
import feign.Logger;
|
||||
import feign.jackson.JacksonDecoder;
|
||||
import feign.jackson.JacksonEncoder;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.consul.client.*;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties
|
||||
public class ConsulAutoConfiguration {
|
||||
protected Feign.Builder builder = Feign.builder()
|
||||
.logger(new Logger.JavaLogger())
|
||||
.errorDecoder(new ConsulErrorDecoder())
|
||||
.decoder(new JacksonDecoder())
|
||||
.encoder(new JacksonEncoder());
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ConsulProperties consulProperties() {
|
||||
return new ConsulProperties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public AgentClient agentClient() {
|
||||
return builder.target(AgentClient.class, consulProperties().getUrl());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public CatalogClient catalogClient() {
|
||||
return builder.target(CatalogClient.class, consulProperties().getUrl());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public KeyValueClient kvClient() {
|
||||
return builder.target(KeyValueClient.class, consulProperties().getUrl());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public EventClient eventClient() {
|
||||
return builder.target(EventClient.class, consulProperties().getUrl());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public EventService eventService() {
|
||||
return new EventService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ConsulEndpoint consulEndpoint() {
|
||||
return new ConsulEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ConsulHealthIndicator consulHealthIndicator() {
|
||||
return new ConsulHealthIndicator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.springframework.cloud.consul;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.consul.client.AgentClient;
|
||||
import org.springframework.cloud.consul.client.CatalogClient;
|
||||
import org.springframework.cloud.consul.client.KeyValueClient;
|
||||
import org.springframework.cloud.consul.model.KeyValue;
|
||||
import org.springframework.cloud.consul.model.Service;
|
||||
import org.springframework.cloud.consul.model.ServiceNode;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "endpoints.consul", ignoreUnknownFields = false)
|
||||
public class ConsulEndpoint extends AbstractEndpoint<ConsulEndpoint.ConsulData> {
|
||||
|
||||
@Autowired
|
||||
KeyValueClient keyValueClient;
|
||||
|
||||
@Autowired
|
||||
CatalogClient catalogClient;
|
||||
|
||||
@Autowired
|
||||
AgentClient agentClient;
|
||||
|
||||
@Autowired
|
||||
public ConsulEndpoint() {
|
||||
super("consul", false, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsulData invoke() {
|
||||
ConsulData data = new ConsulData();
|
||||
//data.setKeyValues(kvClient.getKeyValueRecurse());
|
||||
data.setCatalogServices(catalogClient.getServices());
|
||||
Map<String, Service> services = agentClient.getServices();
|
||||
data.setAgentServices(services);
|
||||
|
||||
for (String serviceId : services.keySet()) {
|
||||
data.getCatalogServiceNodes().put(serviceId, catalogClient.getServiceNodes(serviceId));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ConsulData {
|
||||
Map<String, List<String>> catalogServices;
|
||||
|
||||
Map<String, List<ServiceNode>> catalogServiceNodes = new LinkedHashMap<>();
|
||||
|
||||
Map<String, Service> agentServices;
|
||||
|
||||
List<KeyValue> keyValues;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.springframework.cloud.consul;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.cloud.consul.client.CatalogClient;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulHealthIndicator extends AbstractHealthIndicator {
|
||||
|
||||
@Autowired
|
||||
private CatalogClient catalogClient;
|
||||
|
||||
@Override
|
||||
protected void doHealthCheck(Health.Builder builder) throws Exception {
|
||||
try {
|
||||
Map<String, List<String>> services = catalogClient.getServices();
|
||||
builder.up().withDetail("services", services);
|
||||
} catch (Exception e) {
|
||||
builder.down(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.springframework.cloud.consul;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@ConfigurationProperties("consul")
|
||||
@Data
|
||||
public class ConsulProperties {
|
||||
@NotNull
|
||||
private String url = "http://localhost:8500";
|
||||
|
||||
private List<String> tags = new ArrayList<>();
|
||||
|
||||
private boolean enabled = true;
|
||||
|
||||
private String prefix = "config";
|
||||
|
||||
private List<String> managementTags = Arrays.asList("management");
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import feign.RequestLine;
|
||||
import org.springframework.cloud.consul.model.Service;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public interface AgentClient {
|
||||
@RequestLine("GET /v1/agent/services")
|
||||
Map<String, Service> getServices();
|
||||
|
||||
@RequestLine("GET /v1/agent/self")
|
||||
//TODO change map to an object
|
||||
Map<String, Object> getSelf();
|
||||
|
||||
@RequestLine("PUT /v1/agent/service/register")
|
||||
void register(Service service);
|
||||
|
||||
@RequestLine("PUT /v1/agent/service/deregister/{serviceId}")
|
||||
void deregister(@Named("serviceId") String serviceId);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import feign.RequestLine;
|
||||
import org.springframework.cloud.consul.model.ServiceNode;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public interface CatalogClient {
|
||||
@RequestLine("GET /v1/catalog/services")
|
||||
Map<String, List<String>> getServices();
|
||||
|
||||
@RequestLine("GET /v1/catalog/service/{serviceId}")
|
||||
List<ServiceNode> getServiceNodes(@Named("serviceId") String serviceId);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import feign.Response;
|
||||
import feign.codec.ErrorDecoder;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulErrorDecoder extends ErrorDecoder.Default {
|
||||
@Override
|
||||
public Exception decode(String methodKey, Response response) {
|
||||
if (response.status() == 404) {
|
||||
throw new NotFoundException(response);
|
||||
}
|
||||
return super.decode(methodKey, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import feign.RequestLine;
|
||||
import feign.Response;
|
||||
import org.springframework.cloud.consul.model.Event;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public interface EventClient {
|
||||
//?node=, ?service=, and ?tag= ?dc=
|
||||
@RequestLine("PUT /v1/event/fire/{name}")
|
||||
Event fire(@Named("name") String name, String payload);
|
||||
|
||||
//?name=
|
||||
//?wait=<interval>&index=<idx>
|
||||
@RequestLine("GET /v1/event/list")
|
||||
List<Event> getEvents();
|
||||
|
||||
@RequestLine("GET /v1/event/list")
|
||||
Response getEventsResponse();
|
||||
|
||||
@RequestLine("GET /v1/event/list?wait={wait}&index={index}")
|
||||
Response watch(@Named("wait") String wait, @Named("index") String index);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import com.google.common.base.Throwables;
|
||||
import feign.Response;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.consul.model.Event;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.inject.Named;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class EventService {
|
||||
|
||||
@Autowired
|
||||
protected EventClient client;
|
||||
|
||||
@Autowired(required = false)
|
||||
protected ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private AtomicReference<BigInteger> lastIndex = new AtomicReference<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Response response = getEventsResponse();
|
||||
setLastIndex(response);
|
||||
}
|
||||
|
||||
private void setLastIndex(Response response) {
|
||||
Collection<String> header = response.headers().get("X-Consul-Index");
|
||||
if (header != null && header.iterator().hasNext()) {
|
||||
lastIndex.set(new BigInteger(header.iterator().next()));
|
||||
}
|
||||
}
|
||||
|
||||
public BigInteger getLastIndex() {
|
||||
return lastIndex.get();
|
||||
}
|
||||
|
||||
public Event fire(@Named("name") String name, String payload) {
|
||||
return client.fire(name, payload);
|
||||
}
|
||||
|
||||
public Response getEventsResponse() {
|
||||
return client.getEventsResponse();
|
||||
}
|
||||
|
||||
public List<Event> getEvents() {
|
||||
return client.getEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* from https://github.com/armon/consul-api/blob/master/event.go#L92-L104
|
||||
// IDToIndex is a bit of a hack. This simulates the index generation to
|
||||
// convert an event ID into a WaitIndex.
|
||||
func (e *Event) IDToIndex(uuid string) uint64 {
|
||||
lower := uuid[0:8] + uuid[9:13] + uuid[14:18]
|
||||
upper := uuid[19:23] + uuid[24:36]
|
||||
lowVal, err := strconv.ParseUint(lower, 16, 64)
|
||||
if err != nil {
|
||||
panic("Failed to convert " + lower)
|
||||
}
|
||||
highVal, err := strconv.ParseUint(upper, 16, 64)
|
||||
if err != nil {
|
||||
panic("Failed to convert " + upper)
|
||||
}
|
||||
return lowVal ^ highVal
|
||||
//^ bitwise XOR integers
|
||||
}
|
||||
*/
|
||||
public BigInteger toIndex(String eventId) {
|
||||
String lower = eventId.substring(0, 8) + eventId.substring(9, 13) + eventId.substring(14, 18);
|
||||
String upper = eventId.substring(19, 23) + eventId.substring(24, 36);
|
||||
BigInteger lowVal = new BigInteger(lower, 16);
|
||||
BigInteger highVal = new BigInteger(upper, 16);
|
||||
BigInteger index = lowVal.xor(highVal);
|
||||
return index;
|
||||
}
|
||||
|
||||
public List<Event> getEvents(BigInteger lastIndex) {
|
||||
return filterEvents(readEvents(getEventsResponse()), lastIndex);
|
||||
}
|
||||
|
||||
public List<Event> watch() {
|
||||
return watch(lastIndex.get());
|
||||
}
|
||||
|
||||
public List<Event> watch(BigInteger lastIndex) {
|
||||
//TODO: parameterized or configurable watch time
|
||||
return filterEvents(readEvents(client.watch("2s", lastIndex.toString())), lastIndex);
|
||||
}
|
||||
|
||||
protected List<Event> readEvents(Response response) {
|
||||
try {
|
||||
setLastIndex(response);
|
||||
return objectMapper.readValue(response.body().asInputStream(),
|
||||
TypeFactory.defaultInstance().constructCollectionType(ArrayList.class, Event.class));
|
||||
} catch (IOException e) {
|
||||
Throwables.propagate(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* from https://github.com/hashicorp/consul/blob/master/watch/funcs.go#L169-L194
|
||||
*/
|
||||
protected List<Event> filterEvents(List<Event> toFilter, BigInteger lastIndex) {
|
||||
List<Event> events = toFilter;
|
||||
if (lastIndex != null) {
|
||||
for (int i = 0; i < events.size(); i++) {
|
||||
Event event = events.get(i);
|
||||
BigInteger eventIndex = toIndex(event.getId());
|
||||
if (eventIndex.equals(lastIndex)) {
|
||||
events = events.subList(i + 1, events.size());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import feign.RequestLine;
|
||||
import org.springframework.cloud.consul.model.KeyValue;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public interface KeyValueClient {
|
||||
@RequestLine("GET /v1/kv/{key}")
|
||||
List<KeyValue> getKeyValue(@Named("key") String key);
|
||||
|
||||
@RequestLine("GET /v1/kv/?recurse=true")
|
||||
List<KeyValue> getKeyValueRecurse();
|
||||
|
||||
@RequestLine("GET /v1/kv/{key}?recurse=true")
|
||||
List<KeyValue> getKeyValueRecurse(@Named("key") String key);
|
||||
|
||||
@RequestLine("GET /v1/kv/?keys=true")
|
||||
List<String> getKeys();
|
||||
|
||||
@RequestLine("GET /v1/kv/{key}?keys=true")
|
||||
List<String> getKeys(@Named("key") String key);
|
||||
|
||||
@RequestLine("PUT /v1/kv/{key}")
|
||||
boolean put(@Named("key") String key, Object value);
|
||||
|
||||
@RequestLine("DELETE /v1/kv/{key}")
|
||||
void delete(@Named("key") String key);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import feign.Response;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Data
|
||||
public class NotFoundException extends RuntimeException {
|
||||
private final Response response;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.springframework.cloud.consul.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Data
|
||||
public class Check {
|
||||
@JsonProperty("Script")
|
||||
private String script;
|
||||
|
||||
@JsonProperty("Interval")
|
||||
private int interval;
|
||||
|
||||
@JsonProperty("TTL")
|
||||
private int ttl;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.springframework.cloud.consul.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import static com.google.common.base.Charsets.UTF_8;
|
||||
import static com.google.common.base.MoreObjects.toStringHelper;
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* Example:
|
||||
* "ID": "b54fe110-7af5-cafc-d1fb-afc8ba432b1c",
|
||||
"Name": "deploy",
|
||||
"Payload": null,
|
||||
"NodeFilter": "",
|
||||
"ServiceFilter": "",
|
||||
"TagFilter": "",
|
||||
"Version": 1,
|
||||
"LTime": 0
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class Event {
|
||||
@JsonProperty("ID")
|
||||
private String id;
|
||||
|
||||
@JsonProperty("Name")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("NodeFilter")
|
||||
private String nodeFilter;
|
||||
|
||||
@JsonProperty("ServiceFilter")
|
||||
private String serviceFilter;
|
||||
|
||||
@JsonProperty("TagFilter")
|
||||
private String tagFilter;
|
||||
|
||||
@JsonProperty("Version")
|
||||
private Long version;
|
||||
|
||||
@JsonProperty("LTime")
|
||||
private Long lTime;
|
||||
|
||||
@JsonProperty("Payload")
|
||||
private String payload;
|
||||
|
||||
public String getDecoded() {
|
||||
if (payload == null)
|
||||
return null;
|
||||
return new String(base64().decode(payload), UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toStringHelper(this)
|
||||
.add("id", id)
|
||||
.add("name", name)
|
||||
.add("nodeFilter", nodeFilter)
|
||||
.add("serviceFilter", serviceFilter)
|
||||
.add("tagFilter", tagFilter)
|
||||
.add("version", version)
|
||||
.add("lTime", lTime)
|
||||
.add("payload", payload)
|
||||
.add("decodedPayload", getDecoded())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.springframework.cloud.consul.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import static com.google.common.base.Charsets.UTF_8;
|
||||
import static com.google.common.base.MoreObjects.toStringHelper;
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Data
|
||||
public class KeyValue {
|
||||
@JsonProperty("Key")
|
||||
private String key;
|
||||
|
||||
@JsonProperty("Value")
|
||||
private String value;
|
||||
|
||||
@JsonProperty("CreateIndex")
|
||||
private Long createIndex;
|
||||
|
||||
@JsonProperty("ModifyIndex")
|
||||
private Long modifyIndex;
|
||||
|
||||
@JsonProperty("Flags")
|
||||
private Long flags;
|
||||
|
||||
//TODO: use jackson to do the encoded/decoding
|
||||
public String getDecoded() {
|
||||
if (value == null)
|
||||
return null;
|
||||
return new String(base64().decode(value), UTF_8);
|
||||
}
|
||||
|
||||
public void setUnencoded(String unencoded) {
|
||||
setValue(base64().encode(unencoded.getBytes(UTF_8)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toStringHelper(this)
|
||||
.add("key", key)
|
||||
.add("value", value)
|
||||
.add("decodedValue", getDecoded())
|
||||
.add("createIndex", createIndex)
|
||||
.add("modifyIndex", modifyIndex)
|
||||
.add("flags", flags)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.springframework.cloud.consul.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class Service {
|
||||
@JsonProperty("ID")
|
||||
private String id;
|
||||
|
||||
@JsonProperty("Name")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("Tags")
|
||||
private List<String> tags;
|
||||
|
||||
@JsonProperty("Port")
|
||||
private int port;
|
||||
|
||||
@JsonProperty("Check")
|
||||
private Check check;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.springframework.cloud.consul.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ServiceNode {
|
||||
@JsonProperty("Node")
|
||||
private String node;
|
||||
|
||||
@JsonProperty("Address")
|
||||
private String address;
|
||||
|
||||
@JsonProperty("ServiceID")
|
||||
private String serviceID;
|
||||
|
||||
@JsonProperty("ServiceName")
|
||||
private String serviceName;
|
||||
|
||||
@JsonProperty("ServiceTags")
|
||||
private List<String> serviceTags;
|
||||
|
||||
@JsonProperty("ServicePort")
|
||||
private int servicePort;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Auto Configuration
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.consul.ConsulAutoConfiguration
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.cloud.consul.model.Check;
|
||||
import org.springframework.cloud.consul.model.Service;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* Date: 4/18/14
|
||||
* Time: 11:04 AM
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
@SpringApplicationConfiguration(classes = TestClientConfiguration.class)
|
||||
public class AgentClientIT {
|
||||
|
||||
@Autowired
|
||||
AgentClient client;
|
||||
|
||||
@Test
|
||||
public void test001RegisterService() {
|
||||
Service service = new Service();
|
||||
service.setId("test1id");
|
||||
service.setName("test1Name");
|
||||
service.setPort(9999);
|
||||
service.setTags(Lists.newArrayList("test1tag1", "test1tag2"));
|
||||
Check check = new Check();
|
||||
check.setScript("/usr/local/bin/gtrue");
|
||||
check.setInterval(60);
|
||||
service.setCheck(check);
|
||||
client.register(service);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test002GetServices() {
|
||||
Map<String, Service> services = client.getServices();
|
||||
assertNotNull("services was null", services);
|
||||
assertFalse("services was empty", services.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test003DeregisterService() {
|
||||
client.deregister("test1id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test004GetSelf() {
|
||||
Map<String, Object> self = client.getSelf();
|
||||
assertNotNull("self was null", self);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.cloud.consul.model.ServiceNode;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* Date: 4/18/14
|
||||
* Time: 11:04 AM
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = TestClientConfiguration.class)
|
||||
public class CatalogClientIT {
|
||||
|
||||
@Autowired
|
||||
CatalogClient client;
|
||||
|
||||
@Test
|
||||
public void testGetServices() {
|
||||
Map<String, List<String>> services = client.getServices();
|
||||
assertNotNull("services is null", services);
|
||||
assertTrue("No consul key", services.containsKey("consul"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetService() {
|
||||
List<ServiceNode> serviceNodes = client.getServiceNodes("consul");
|
||||
assertNotNull("serviceNodes is null", serviceNodes);
|
||||
assertFalse("serviceNodes is empty", serviceNodes.isEmpty());
|
||||
|
||||
ServiceNode node = serviceNodes.get(0);
|
||||
|
||||
assertNotNull("address is null", node.getAddress());
|
||||
assertNotNull("node is null", node.getNode());
|
||||
assertNotNull("serviceId is null", node.getServiceID());
|
||||
assertNotNull("serviceName is null", node.getServiceName());
|
||||
assertTrue("servicePort is wrong", node.getServicePort() > 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.cloud.consul.model.Event;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = TestClientConfiguration.class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
public class EventClientIT {
|
||||
|
||||
public static final String NAME = "testEvent";
|
||||
public static final String PAYLOAD = "TestPayload." + System.currentTimeMillis();
|
||||
|
||||
@Autowired
|
||||
private EventClient client;
|
||||
|
||||
@Test
|
||||
public void test001Fire() {
|
||||
Event event = client.fire(NAME, PAYLOAD);
|
||||
assertNotNull("Event was null", event);
|
||||
assertNotNull("Event Id was null", event.getId());
|
||||
assertEquals("Event name was wrong", NAME, event.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test002Get() {
|
||||
List<Event> values = client.getEvents();
|
||||
assertNotNull("events is null", values);
|
||||
assertFalse("Values is empty", values.isEmpty());
|
||||
/*assertTrue("Values is not size 1", values.size() == 1);
|
||||
KeyValue keyValue = values.get(0);
|
||||
//TODO: how to deal with this?
|
||||
String decoded = objectMapper.readValue(keyValue.getDecoded(), String.class);
|
||||
|
||||
assertEquals(decoded, VALUE);*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.cloud.consul.model.Event;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = TestClientConfiguration.class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
public class EventServiceIT {
|
||||
|
||||
@Autowired
|
||||
private EventService service;
|
||||
|
||||
@Test
|
||||
public void test001InitialIndex() {
|
||||
assertNotNull("initialIndex was null", service.getLastIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test002ToIndex() {
|
||||
String eventId = "bf24ae36-d240-9666-7343-1a87346d2f94";
|
||||
BigInteger index = service.toIndex(eventId);
|
||||
assertEquals("wrong index generated", new BigInteger("14728939782502463986"), index);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test003GetEvents() {
|
||||
Event event = service.fire("testEvent", "test003GetEvents" + System.currentTimeMillis());
|
||||
assertNotNull("event was null", event);
|
||||
|
||||
List<Event> events = service.getEvents(service.getLastIndex());
|
||||
assertNotNull("events was null", events);
|
||||
assertFalse("events was empty", events.isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.cloud.consul.model.KeyValue;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = TestClientConfiguration.class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
public class KeyValueClientIT {
|
||||
|
||||
public static final String KEY = "test/testkey";
|
||||
public static final String VALUE = "TestPut." + System.currentTimeMillis();
|
||||
|
||||
@Autowired
|
||||
KeyValueClient keyValueClient;
|
||||
|
||||
@Autowired(required = false)
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
public void test001Put() {
|
||||
|
||||
boolean actual = keyValueClient.put(KEY, VALUE);
|
||||
assertTrue("Invalid resposne", actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test002Get() throws IOException {
|
||||
List<KeyValue> values = keyValueClient.getKeyValue(KEY);
|
||||
assertNotNull("values is null", values);
|
||||
assertFalse("Values is null", values.isEmpty());
|
||||
assertTrue("Values is not size 1", values.size() == 1);
|
||||
KeyValue keyValue = values.get(0);
|
||||
//TODO: how to deal with this?
|
||||
String decoded = objectMapper.readValue(keyValue.getDecoded(), String.class);
|
||||
|
||||
assertEquals(decoded, VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test003GetKeyRecurse() {
|
||||
List<KeyValue> values = keyValueClient.getKeyValueRecurse(KEY);
|
||||
assertNotNull("values is null", values);
|
||||
assertFalse("Values is null", values.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test004GetRecurse() {
|
||||
List<KeyValue> values = keyValueClient.getKeyValueRecurse();
|
||||
assertNotNull("values is null", values);
|
||||
assertFalse("Values is null", values.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test005Delete() {
|
||||
keyValueClient.delete(KEY);
|
||||
}
|
||||
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void test006KeyNotFound() {
|
||||
keyValueClient.getKeyValue(System.currentTimeMillis()+"a123");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.springframework.cloud.consul.client;
|
||||
|
||||
import org.springframework.cloud.consul.ConsulAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration
|
||||
@Import(ConsulAutoConfiguration.class)
|
||||
public class TestClientConfiguration {
|
||||
}
|
||||
48
spring-cloud-consul-discovery/pom.xml
Normal file
48
spring-cloud-consul-discovery/pom.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-consul-discovery</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring Cloud Consul Discovery</name>
|
||||
<description>Spring Cloud Consul Discovery</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.ribbon</groupId>
|
||||
<artifactId>ribbon</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.ribbon</groupId>
|
||||
<artifactId>ribbon-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<!-- Only needed at compile time -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.springframework.cloud.consul.discovery;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration
|
||||
public class ConsulDiscoveryAutoConfiguration {
|
||||
@Bean
|
||||
public ConsulLifecycle consulLifecycle() {
|
||||
return new ConsulLifecycle();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConsulLoadBalancerClient consulLoadBalancerClient() {
|
||||
return new ConsulLoadBalancerClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConsulDiscoveryClient consulDiscoveryClient() {
|
||||
return new ConsulDiscoveryClient();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.springframework.cloud.consul.discovery;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.consul.client.AgentClient;
|
||||
import org.springframework.cloud.consul.client.CatalogClient;
|
||||
import org.springframework.cloud.consul.model.Service;
|
||||
import org.springframework.cloud.consul.model.ServiceNode;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.google.common.collect.Iterables.*;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulDiscoveryClient implements DiscoveryClient {
|
||||
|
||||
@Autowired
|
||||
ApplicationContext context;
|
||||
|
||||
@Autowired
|
||||
AgentClient agentClient;
|
||||
|
||||
@Autowired
|
||||
CatalogClient catalogClient;
|
||||
|
||||
@Override
|
||||
public ServiceInstance getLocalServiceInstance() {
|
||||
Map<String, Service> services = agentClient.getServices();
|
||||
Service service = services.get(context.getId());
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("Unable to locate service in consul agent: "+context.getId());
|
||||
}
|
||||
String host = "localhost";
|
||||
Map<String, Object> self = agentClient.getSelf();
|
||||
Map<String, Object> member = (Map<String, Object>) self.get("Member");
|
||||
if (member != null) {
|
||||
if (member.containsKey("Name")) {
|
||||
host = (String) member.get("Name");
|
||||
}
|
||||
}
|
||||
return new DefaultServiceInstance(service.getId(), host, service.getPort());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ServiceInstance> getInstances(final String serviceId) {
|
||||
List<ServiceNode> nodes = catalogClient.getServiceNodes(serviceId);
|
||||
Iterable<ServiceInstance> instances = transform(nodes, new Function<ServiceNode, ServiceInstance>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public ServiceInstance apply(@Nullable ServiceNode node) {
|
||||
return new DefaultServiceInstance(serviceId, node.getNode(), node.getServicePort());
|
||||
}
|
||||
});
|
||||
|
||||
return Lists.newArrayList(instances);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ServiceInstance> getAllInstances() {
|
||||
Iterable<ServiceInstance> instances = transform(concat(transform(catalogClient.getServices().keySet(), new Function<String, List<ServiceNode>>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public List<ServiceNode> apply(@Nullable String input) {
|
||||
return catalogClient.getServiceNodes(input);
|
||||
}
|
||||
})), new Function<ServiceNode, ServiceInstance>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public ServiceInstance apply(@Nullable ServiceNode input) {
|
||||
return new DefaultServiceInstance(input.getServiceName(), input.getNode(), input.getServicePort());
|
||||
}
|
||||
});
|
||||
|
||||
return Lists.newArrayList(instances);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getServices() {
|
||||
return Lists.newArrayList(catalogClient.getServices().keySet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.springframework.cloud.consul.discovery;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.client.discovery.AbstractDiscoveryLifecycle;
|
||||
import org.springframework.cloud.consul.ConsulProperties;
|
||||
import org.springframework.cloud.consul.client.AgentClient;
|
||||
import org.springframework.cloud.consul.model.Service;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Slf4j
|
||||
public class ConsulLifecycle extends AbstractDiscoveryLifecycle {
|
||||
|
||||
@Autowired
|
||||
private AgentClient agentClient;
|
||||
|
||||
@Autowired
|
||||
private ConsulProperties consulProperties;
|
||||
|
||||
@Override
|
||||
protected void register() {
|
||||
Service service = new Service();
|
||||
String appName = getAppName();
|
||||
service.setId(context.getId());
|
||||
service.setName(appName);
|
||||
//TODO: support port = 0 random assignment
|
||||
Integer port = new Integer(environment.getProperty("server.port", "8080"));
|
||||
service.setPort(port);
|
||||
service.setTags(consulProperties.getTags());
|
||||
|
||||
//TODO: add support for Check
|
||||
register(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void registerManagement() {
|
||||
Service management = new Service();
|
||||
management.setId(getManagementServiceId());
|
||||
management.setName(getManagementServiceName());
|
||||
management.setPort(getManagementPort());
|
||||
management.setTags(consulProperties.getManagementTags());
|
||||
|
||||
register(management);
|
||||
}
|
||||
|
||||
protected void register(Service service) {
|
||||
log.info("Registering service with consul: {}", service.toString());
|
||||
agentClient.register(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getConfiguration() {
|
||||
return consulProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deregister() {
|
||||
deregister(context.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deregisterManagement() {
|
||||
deregister(getManagementServiceName());
|
||||
}
|
||||
|
||||
private void deregister(String serviceId) {
|
||||
agentClient.deregister(serviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isEnabled() {
|
||||
return consulProperties.isEnabled();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.springframework.cloud.consul.discovery;
|
||||
|
||||
import com.netflix.client.config.DefaultClientConfigImpl;
|
||||
import com.netflix.client.config.IClientConfig;
|
||||
import com.netflix.loadbalancer.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
|
||||
import org.springframework.cloud.consul.client.CatalogClient;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulLoadBalancerClient implements LoadBalancerClient {
|
||||
|
||||
private ConcurrentHashMap<String, ILoadBalancer> namedLoadBalancers = new ConcurrentHashMap<>();
|
||||
private ConcurrentHashMap<String, IClientConfig> namedClientConfigs = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
CatalogClient catalogClient;
|
||||
|
||||
@Override
|
||||
public ServiceInstance choose(String serviceId) {
|
||||
ILoadBalancer lb = namedLoadBalancers.get(serviceId);
|
||||
if (lb == null) {
|
||||
IClientConfig config = namedClientConfigs.get(serviceId);
|
||||
|
||||
if (config == null) {
|
||||
DefaultClientConfigImpl clientConfig = new DefaultClientConfigImpl();
|
||||
clientConfig.setClientName(serviceId);
|
||||
config = clientConfig;
|
||||
namedClientConfigs.put(serviceId, clientConfig);
|
||||
}
|
||||
lb = LoadBalancerBuilder.<ConsulServer>newBuilder()
|
||||
.withClientConfig(config)
|
||||
//TODO: config to choose rules
|
||||
.withRule(new AvailabilityFilteringRule())
|
||||
//TODO: figure out ping
|
||||
//.withPing()
|
||||
.withDynamicServerList(new ConsulServerList(catalogClient, serviceId))
|
||||
.buildDynamicServerListLoadBalancer();
|
||||
namedLoadBalancers.put(serviceId, lb);
|
||||
}
|
||||
Server server = lb.chooseServer(null);
|
||||
return new DefaultServiceInstance(server.getId(), server.getHost(), server.getPort());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T choose(String serviceId, LoadBalancerRequest<T> request) {
|
||||
return request.apply(choose(serviceId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.springframework.cloud.consul.discovery;
|
||||
|
||||
import com.netflix.loadbalancer.Server;
|
||||
import org.springframework.cloud.consul.model.ServiceNode;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulServer extends Server {
|
||||
|
||||
private final MetaInfo metaInfo;
|
||||
|
||||
public ConsulServer(final ServiceNode node) {
|
||||
super(node.getNode(), node.getServicePort());
|
||||
metaInfo = new MetaInfo() {
|
||||
@Override
|
||||
public String getAppName() {
|
||||
return node.getServiceName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerGroup() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceIdForDiscovery() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInstanceId() {
|
||||
return node.getServiceID();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public MetaInfo getMetaInfo() {
|
||||
return metaInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.springframework.cloud.consul.discovery;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.netflix.client.config.IClientConfig;
|
||||
import com.netflix.loadbalancer.AbstractServerList;
|
||||
import org.springframework.cloud.consul.client.CatalogClient;
|
||||
import org.springframework.cloud.consul.model.ServiceNode;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.common.collect.Collections2.transform;
|
||||
import static com.google.common.collect.Lists.newArrayList;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulServerList extends AbstractServerList<ConsulServer> {
|
||||
|
||||
private CatalogClient client;
|
||||
|
||||
private String serviceId;
|
||||
|
||||
|
||||
public ConsulServerList(CatalogClient client, String serviceId) {
|
||||
this.client = client;
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initWithNiwsConfig(IClientConfig clientConfig) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ConsulServer> getInitialListOfServers() {
|
||||
return getServers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ConsulServer> getUpdatedListOfServers() {
|
||||
return getServers();
|
||||
}
|
||||
|
||||
private List<ConsulServer> getServers() {
|
||||
List<ServiceNode> nodes = client.getServiceNodes(serviceId);
|
||||
if (nodes == null || nodes.isEmpty()) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
Collection<ConsulServer> servers = transform(nodes, new Function<ServiceNode, ConsulServer>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public ConsulServer apply(@Nullable ServiceNode node) {
|
||||
ConsulServer server = new ConsulServer(node);
|
||||
return server;
|
||||
}
|
||||
});
|
||||
|
||||
return newArrayList(servers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Auto Configuration
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.consul.discovery.ConsulDiscoveryAutoConfiguration
|
||||
51
spring-cloud-consul-sample/pom.xml
Normal file
51
spring-cloud-consul-sample/pom.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-consul-sample</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring Cloud Consul Sample</name>
|
||||
<description>Spring Cloud Consul Sample</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul-config</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul-discovery</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-consul-bus</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.springframework.cloud.consul.sample;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.bind.RelaxedPropertyResolver;
|
||||
import org.springframework.cloud.bus.jackson.SubtypeModule;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.consul.bus.SimpleRemoteEvent;
|
||||
import org.springframework.cloud.consul.discovery.ConsulLoadBalancerClient;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@RestController
|
||||
@Slf4j
|
||||
public class SampleApplication implements ApplicationListener<SimpleRemoteEvent> {
|
||||
|
||||
public static final String CLIENT_NAME = "testConsulApp";
|
||||
|
||||
@Autowired
|
||||
ConsulLoadBalancerClient loadBalancer;
|
||||
|
||||
@Autowired
|
||||
Environment env;
|
||||
|
||||
@Autowired(required = false)
|
||||
RelaxedPropertyResolver resolver;
|
||||
|
||||
@RequestMapping("/")
|
||||
public ServiceInstance lb() {
|
||||
return loadBalancer.choose(CLIENT_NAME);
|
||||
}
|
||||
|
||||
@RequestMapping("/myenv")
|
||||
public String env(@RequestParam("prop") String prop) {
|
||||
String property = new RelaxedPropertyResolver(env).getProperty(prop, "Not Found");
|
||||
return property;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SubtypeModule sampleSubtypeModule() {
|
||||
return new SubtypeModule(SimpleRemoteEvent.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleApplication.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(SimpleRemoteEvent event) {
|
||||
log.info("Received event: {}", event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
#TODO: figure out why I need this here and in bootstrap.yml
|
||||
spring:
|
||||
application:
|
||||
name: testConsulApp
|
||||
|
||||
security:
|
||||
user:
|
||||
name: user
|
||||
password: password
|
||||
|
||||
endpoints:
|
||||
health:
|
||||
sensitive: false
|
||||
restart:
|
||||
enabled: true
|
||||
shutdown:
|
||||
enabled: true
|
||||
@@ -0,0 +1,7 @@
|
||||
spring:
|
||||
application:
|
||||
name: testConsulApp
|
||||
cloud:
|
||||
config:
|
||||
# TODO: refactor spring-cloud-config to use refresh, etc.. with out config client
|
||||
enabled: false
|
||||
Reference in New Issue
Block a user