Compare commits

...

198 Commits

Author SHA1 Message Date
Nikola Marčetić ea79f22269 Changelog updates regarding to 0.8 Milestone (#669)
Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2019-03-20 18:10:32 +01:00
Nikola Marčetić 767c900c48 Bump version to 0.8 (#668)
Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2019-03-20 17:44:38 +01:00
Darko Draskovic d504c4f67d MF-571 - Add Env.elm to set custom base URL (#654)
* Add Env module

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Modify README.md

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove literal vals from Env.elm

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Replace protocol, host and port env setting by url

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove default env.url value and update README.md

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add http://localhost:80/ as a default baseURL val

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Replace Gateflux by Mainflux

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2019-03-20 14:30:38 +01:00
Jovan Kostovski e95e010a3a added docs about docker-compose config overriding (#653)
Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2019-03-19 22:32:07 +01:00
Aleksandar Novaković a09e3e214a NOISSUE - Fix subtopic regex and restrict empty subtopic parts (#659)
* Fix subtopic regex and restrict empty subtopic parts

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update mqtt adapter subtopic processing

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Return old MQTT adapter implementation

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Allow dots and handle empty parts

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add malformed subtopic error encoding

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix MQTT topic validation

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix MQTT topic parsing

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix subtopic filtering

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2019-03-19 16:05:56 +01:00
tony-ward 7c4c6109f4 MF-639 Split Content-Type header field on semicolon and evaluate all substrings (#644)
* Split Content-Type header field on semicolon and evaluate all substrings

Signed-off-by: Tony Ward <tony@lucidtron.com>

* included strings library
Signed-off-by: Tony Ward <tony0ward@hotmail.com>

* simplify code, run gofmt

Signed-off-by: tony <tony0ward@hotmail.com>
Signed-off-by: tony <tony@lucidtron.com>

* allow Content-Type to only contain the required type

Signed-off-by: Tony Ward <tony@lucidtron.com>
2019-03-19 12:00:11 +01:00
Aleksandar Novaković 8c3bff5f4b MF-656 - Change bootstrap service port to 8200 (#658)
* Change bootstrap service port to 8200

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix redis streams tests in things service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2019-03-18 13:50:44 +01:00
Dušan Borovčanin 2ed1471d5f MF-539 - Improve Bootstrap Service documentation (#646)
* Fix infinte loop in Subscribe method

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update API docs

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update service README

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update docs

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add bootstrapping flow gif

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update bootstrapping flow gif

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2019-03-15 19:11:26 +01:00
beres 61b2d6b87b MF-596 - Add subtopic to RawMessage (#642)
* Commit for mainflux-596
Modified and tested:
- cli
- http
- mqtt
- normalizer
- all readers
- sdk messages
- all writers
- ws
Missing:
- coap
- lora

Signed-off-by: ale <ale@metaverso.org>

* - fix subtopic name in, when starting with dot, http/ws/mqtt
- add some test on readers

Signed-off-by: ale <ale@metaverso.org>

* - fix string concatenation
- update http/transport regexp to match subtopic names with only \w-
- update ws/transport regexp like http ones with also the wildcard * and >

Signed-off-by: ale <ale@metaverso.org>

* added subtopic support to coap adapter

Signed-off-by: ale <ale@metaverso.org>

* - update replace functions with replaceall when needed
- renamed getDestChannel to fmtSubject
- update api/transport and ws/transport route to be more readable
- fix mqtt syntax
- renamed func andQuery to query as suggested by @anovakovic01
- have a nice we :)

Signed-off-by: ale <ale@metaverso.org>

* - fix error declaration on ws/nat/publisher
- fix regexp added missing allowed chars - and _ on coap/api/transport
- fix subtopic clean suffix / if present on coap/api/transport
- improve regexp on http and ws /api/transport, now does not accept url that do not strictly match
- add some ws subtopic tests

Signed-off-by: ale <ale@metaverso.org>

* - enabled wildcard chars on coap/api/transport
- allow use special chars on http and ws api/transport

Signed-off-by: ale <ale@metaverso.org>

* - use strings.Replace() insted ReplaceAll()

Signed-off-by: ale <ale@metaverso.org>

* - allow every chars on subtopics
- fix replace error on mqtt

Signed-off-by: ale <ale@metaverso.org>

* fix cassandra test

Signed-off-by: ale <ale@metaverso.org>

* fix ws test with invalid subtopic

Signed-off-by: ale <ale@metaverso.org>

* fix invalid GOCACHE in go1.12, replaced by -count 1, see https://golang.org/doc/go1.10#test

Signed-off-by: ale <ale@metaverso.org>

* - improve regexp on http/ws api/transport
- minor changes

Signed-off-by: ale <ale@metaverso.org>

* - add generic function parseSubtopic on ws/http adapters

Signed-off-by: ale <ale@metaverso.org>

* - add generic function fmtSubtopic on coap adapter

Signed-off-by: ale <ale@metaverso.org>
2019-03-15 18:38:07 +01:00
Manuel Imperiale 2f09701028 NOISSUE - Prevent infinite loop in lora-adapter if Redis init fail (#647)
* NOISSUE - Prevent infinite loop in lora-adapter if Redis init fail

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix line length

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2019-03-14 18:10:51 +01:00
Darko Draskovic a0ed73d09b Replace crossOrigin with relative path and fix messaging bug (#645)
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2019-03-13 13:58:03 +01:00
Alwin Doss 74dfd69767 Corrected grammar and rephrased a few sentences to read nicely (#641)
Signed-off-by: Alwin Doss <alwindoss84@gmail.com>

Incorporated review comments

Signed-off-by: Alwin Doss <alwindoss84@gmail.com>

Fixed grammar

Signed-off-by: Alwin Doss <alwindoss84@gmail.com>
2019-03-07 16:25:37 +01:00
Darko Draskovic b2ce9f5597 Fix missing css in container ui (#638)
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2019-03-07 13:39:35 +01:00
Darko Draskovic 3255f32a9c MF-571 - Elm UI (#632)
* Initial commit

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add user create form

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add Makefile and README

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove elm-stuff

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add url parsing

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add channels retrive and remove

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Refactor request f and add f annotations

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add things provision

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add thing retrieve and remove

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Rename Channel.elm methods

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Fix trailing slash and add url constants

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add module User with separate model, view and update

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Modularize channels

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Modularize version and things

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Refactor module methods

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Modularize messaging

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add channel pagination

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Fix initial channel model

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add access control (connect and disconnect things from channel)

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add pagination to Things

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Using Url.Builder in connection and messaging
Rename Acces module to Connection
Rename attribute in Bootstrap code
Remove debug logging

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add login

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add dynamic menu

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Fix empty name channel/thing bug

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove menu from login screen and add response helper

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add version, channels, things direct display

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add channels table

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Change var names and add refreshChannelList helper

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add Things list

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Fix response behavior

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove version page button

Signed-off-by: drasko <drasko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Move everything to ui folder

Signed-off-by: drasko <drasko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove dashflux

Signed-off-by: drasko <drasko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* MF-571 Add things and channels lists to Connection module (#580)

* Add thing and channel lists to Connection module

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Fix form reset bug & remember checkbox status in Connection module

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* MF-571 - Add batch things-channels connection modification (#588)

* Add batch things-channels connection modification

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove unused token arg and reposition buttons

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* NOISSUE - messaging in UI (#578)

* Add thing and channel lists to Connection module

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* init commit

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* list channels and things

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* list channels with radio buttons

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Listing channels for selected thing

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* fix list channels for selected thing

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Use records instead of hardcode values
Use genFormField from Helpers
Remove dead comments

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* NOISSUE - Add Docker build for UI (#582)

* Add Docker build for UI

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>

* Fix entrypoint and ports

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>

* Add NginX server. Fix port.

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* NOISSUE - Fix layout and add custom CSS (#593)

* Fix layout and add custom CSS

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>

* Remove comments, add copyright headers

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>

* Add newline at the end of the css file

Signed-off-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* NOISSUE -update message module (#599)

* Update message module
Sending message as selected thing to multiple channels

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Rename token to thingkey

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Reset model on send message and reset list of channels on selecting thing

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove logout button and add logout header (#604)

* Remove logout button and add logout header

* Removed dead code.

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* MF-571 - Pagination (#606)

* Add total # things to Thing model

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add basic pagination for Thing

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add pagination helper function & Channel pagination

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* NOISSUE - Add bootstrap cards in version view (#607)

* Add total # things to Thing model

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add basic pagination for Thing

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add bootstrap cards in version view

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add pagination helper function & Channel pagination

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* modify styling

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Styling

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Mainflux logo

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Revert "NOISSUE - Add bootstrap cards in version view (#607)"

This reverts commit a9a4dd7c73.

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Pagination (#613)

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* MF-571 - Add dashboard (#614)

* Add Dashboard module and simplify Main module

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove unused code and Version module

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add helper funcs to Main.elm update method

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Improve Main and Message module organization

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add dashboard Things and Channels buttons

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add Modal to edit thing

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add RemoveThing msg and AwesomeFont

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Unify provision and retrieve request

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Reorganize code by making funcs and code repositioning

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add fon-awesome 5.* support

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add metadata edit

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove table header add thing and removed dead code

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* MF-571 - UI modal buttons (#617)

* Add Dashboard module and simplify Main module

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove unused code and Version module

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add helper funcs to Main.elm update method

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Improve Main and Message module organization

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add dashboard Things and Channels buttons

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add Modal to edit thing

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add RemoveThing msg and AwesomeFont

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Unify provision and retrieve request

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Reorganize code by making funcs and code repositioning

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add fon-awesome 5.* support

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add metadata edit

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove table header add thing and removed dead code

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add complete Thing edit modal buttons

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add card style to channels (#618)

Add dropdown user menu
Add font awesome to main menu

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* MF-571 - UI Thing module with modals (#620)

* Add Thing provision modal

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add app/dev dropdown to Thing provision modal

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Refactor Thing to have separate Update and Provision msg handling

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove fontAwesome import and fix Dict import

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* MF-571 - UI channel modals (#621)

* Add Thing provision modal

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add app/dev dropdown to Thing provision modal

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Refactor Thing to have separate Update and Provision msg handling

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove fontAwesome import and fix Dict import

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add provision and edit modals to Channel

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add HttpMF http custom library

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add ModalMF Bootstrap modal custom library

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Moved all http funcs to HttpMF

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Moved expect funcs from Connection and Message to HttpMF

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add cards and change tables (#622)

* Change type of tables
Add tables to cards
Colorize buttons

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* remove comments

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* MF-571 - Fix user login and refactor User (#623)

* Fix user login and refactor User

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove redundant Grid.row nesting

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Fix links in README (#624)

Signed-off-by: Ivan Milošević <iva@blokovi.com>
Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Fix ui/Makefile and ui/README.md

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add copyright header to .elm files

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Fix missing README.md after migration

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Fix typo

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add install options to README.md

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Rename Url to URL and Id to ID

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>
2019-03-06 16:21:09 +01:00
Vladimir Starčević dbd8f9be23 MF-579 Things & Channels returns 404 when not found or ID is malformed, not 500 (#633)
* Things returns 404 when not found, not 500

Signed-off-by: Vladimir Starcevic <vstarcevic@gmail.com>

* MF-579 Things returns 404 when not found or thingid is in invalid format

Signed-off-by: Vladimir Starcevic <vstarcevic@gmail.com>

* MF-579 Things and channels returns 404 when not found, or ID is malformed not 500

Signed-off-by: Vladimir Starcevic <vstarcevic@gmail.com>

* MF-579 Things & Channels returns 404 when not found or ID is malformed, not 500

Signed-off-by: Vladimir Starcevic <vstarcevic@gmail.com>
2019-03-06 14:10:01 +01:00
Dušan Borovčanin 3300814026 MF-552 - Use event sourcing to keep Bootstrap service in sync with Things service (#603)
* Use separate table for Channels

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add inital event sourcing subscription

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add Channel update sync

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add Thing remove sync

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add Channel remove sync

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update service add method marshalling metadata

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Make separate methods for connection update

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add diconnect event sync

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update Configs repository mock

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix service tests

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update repository tests

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update API docs

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update Location header

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update README.md

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix tests mutex lock

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix method names in logs

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2019-03-04 17:41:38 +01:00
Manuel Imperiale a60a5c1ba1 NOISSUE - Fix lora-adapter Object decode (#610)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2019-02-22 15:30:37 +01:00
Dušan Borovčanin fa7d638453 MF-540 - Add pagination in API responses for Bootstrap service (#575)
* Add Page to List response

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add request validation tests

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update endpoint routes

Update API docs accordingly.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add optional Thing ID to config add request

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Extract literals to constants

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update comments

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix count logs

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2019-02-22 14:54:09 +01:00
Manuel Imperiale 1c09632671 MF-600 - Handle custom LoRa Server application decoder (#608)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2019-02-22 09:31:44 +01:00
Ivan Milošević 06921fbee6 Fix run command in dev guide (#605)
Signed-off-by: Ivan Milošević <iva@blokovi.com>
2019-02-21 15:56:51 +01:00
caztial b69d31817d update docker-compose (#590)
add udp instead of default tcp to the docker coap port.
2019-02-20 00:17:13 +01:00
Dušan Borovčanin 225f56fc3e Update generated code (#602)
Use latest release of `protobuf` 3.6.1.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2019-02-18 17:10:25 +01:00
Dušan Borovčanin 133383ecb7 Add generated files check (#601)
Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2019-02-17 18:40:30 +01:00
Mirko Teodorovic 2ee0ed18e0 MF-597 - Removed legacy code as not needed anymore (#598)
* Removed legacy code as not needed anymore

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* Small correction

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>

* Remove git as not needed

Signed-off-by: mteodor <mirko.teodorovic@gmail.com>
2019-02-15 18:35:52 +01:00
mteodor c418a9a4cf NOISSUE - Added normalizer service to run script (#594)
* Added normalizer service to be run

* Update run.sh
2019-02-13 12:05:29 +01:00
Kai-Chu Chung 351d6e7bfb MF-583 - Correct cmd/mongodb-reader HTTPServer log Info (#584)
Signed-off-by: Kai-Chu Chung <cage.chung@gmail.com>
2019-02-12 11:36:58 +01:00
Nikola Marčetić 434a57d8e1 Changed RawMessage (#587)
Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2019-02-11 14:27:56 +01:00
Manuel Imperiale 94574e3707 NOISSUE - fix CLI log (#581)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2019-02-08 15:38:07 +01:00
Juanma Fernandez b12bb3374b NOISSUE - Fix users logs in main.go (#577)
Signed-off-by: juanmagal <juanma.galmes@gmail.com>
2019-02-07 08:43:43 +01:00
Dušan Borovčanin 3f3b42b511 MF-519 - Refine Message (#567)
* Enable Message JSON mashaling/unmarshaling

Update Protobuf generated files.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update readers and SDK to use Mainflux Message

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix alignment

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2019-02-06 13:57:12 +01:00
Dušan Borovčanin 1df4dcd7b7 NOISSUE - Add name field for Bootstrap Config (#564)
* Add name field to Config

Enable search by name.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Create separate response for unknown Configs

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use meaningful names for filters

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add name search tests

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update API docs

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Break mocks check into multiple lines

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Create new instances in a consistent way

Reformat `return` statements.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2019-02-06 10:28:54 +01:00
Aleksandar Novaković 8966a13760 Fix non-SenML message routing in normalizer (#573)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2019-02-04 13:07:05 +01:00
Drasko DRASKOVIC 0fbb21c3fb Fix Dusan Maldenovic GitHub (#570)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2019-02-03 09:12:40 +01:00
Drasko DRASKOVIC 998c7ef9ef NOISSUE - Update authors list (#569)
* NOISSUE - Update authors list

Add Dejan's recognition

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Fix Manu's GitHub nick

Signed-off-by: drasko <drasko.draskovic@gmail.com>
2019-02-02 22:21:33 +01:00
Manuel Imperiale dc16eb5451 Update lora.md (#568)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2019-02-01 23:46:02 +01:00
Manuel Imperiale 53036b5ae7 Fix CLI docs (#566)
* Fix CLI docs

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix CLI docs typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix CLI docs typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix CLI docs typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Typo fix

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Typo fix

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2019-02-01 23:17:09 +01:00
Manuel Imperiale c04909d645 NOISSUE- Improve LoRa doc (#562)
* Improve lora docs

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Typo fix

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Typo fix

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Typo fix

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2019-01-31 18:26:00 +01:00
Dušan Borovčanin bf9e148120 MF-551 - Add metadata fields to Bootstrap Channels (#563)
* Save MF channel name and metadata to the BS

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove separate Channels table

Use nested JSON field instead.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix tests

Use proper JSON format for Bootstrap response fields.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update API docs

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove unnecessary comments

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Extract Config fields to constants

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Inline if statements

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2019-01-30 16:40:37 +01:00
Aleksandar Novaković 330545ad38 Fix MQTT adapter by setting subscription queue (#561)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2019-01-28 22:55:14 +01:00
Nikola Marčetić 1947d39119 MF-558 - Add MQTT subtopics documentation (#559)
* Added MQTT subtopic's doc section

Signed-off-by: nmarcetic <n.marcetic86@gmail.com>

* Resolving remarks

Signed-off-by: nmarcetic <n.marcetic86@gmail.com>

* Resolving remarks

Signed-off-by: nmarcetic <n.marcetic86@gmail.com>

* Resolved remarks

Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2019-01-28 14:36:29 +01:00
Drasko DRASKOVIC 0b770ce49e Fix regexp for SUB (#557)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2019-01-26 20:53:35 +01:00
Drasko DRASKOVIC 2c6379f823 Simplify MQTT topipc regexp (#555)
Current regexp makes 2 selection groups, while
capturing just the first group (the only one of
interest) into variable.

This PR simplifies the regexp and avoids capturing
secound group.

Signed-off-by: drasko <drasko.draskovic@gmail.com>
2019-01-26 19:53:01 +01:00
Nikola Marčetić c8e7a97122 MF-429 -Enabled MQTT subtopic's (#554)
* Enabled MQTT subtopic

Signed-off-by: nmarcetic <n.marcetic86@gmail.com>

* Removed debug logs

Signed-off-by: nmarcetic <n.marcetic86@gmail.com>

* Resolved remarks

Signed-off-by: nmarcetic <n.marcetic86@gmail.com>

* Resolved remarks

Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2019-01-25 22:22:36 +01:00
Manuel Imperiale 6bc287e028 NOISSUE - Fix normalizer exposed port in docker-compose (#548)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2019-01-21 09:28:50 +01:00
Aleksandar Novaković f9f25e2d2f Fix pagination response for empty page (#547)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2019-01-17 17:31:30 +01:00
Aleksandar Novaković ca6968f8d0 Fix swagger and provisioning docs (#546)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2019-01-17 16:56:03 +01:00
Aleksandar Novaković a618627599 Add env var for number of concurrent messages (#545)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2019-01-17 02:26:58 +01:00
Aleksandar Novaković fedbe1c422 NOISSUE - Update doc and fix empty key bug (#544)
* Update docs with new connection endpoints

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix empty key bug

Return key that was absent from things that are returned in
connections endpoint.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2019-01-15 18:51:06 +01:00
Dušan Borovčanin 7ff738360b MF-370 - Simplify and refine CI (#541)
* Add CI script

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix indentetion

Add script explanation comment.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2019-01-11 13:57:47 +01:00
Manuel Imperiale a0505e0eeb NOISSUE - Add connection commands to CLI (#542)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2019-01-10 21:42:31 +01:00
Drasko DRASKOVIC 3bbd21f583 NOISSUE - Refine docs (#537)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2019-01-09 17:04:34 +01:00
Dušan Borovčanin c788ef64c0 Update licnese year (#533)
Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2019-01-09 16:23:30 +01:00
Dušan Borovčanin 22fc26b375 MF-513 - Add Bootstrapping service (#524)
* Introduce Config response for bootstrap procedure

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add inital service implementation

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Enable status change

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix logger import

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update BSS to send config in valid format

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use ConfigReader to create valid format response

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update config retrieval error handle

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Enable Thing deletion API

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add API support for fetching Thing by ID

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add list Things endpoint

Update database schema

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use MF API to update status

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use Channels list

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix reading Thing from the database

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Create Mainflux Thing when adding new Thing to BS

Create MF Thing as soon as Bootstrap service thing is added. There are 2
main reasons to create Thing when adding a new BS Thing over creating
Thing on bootstrapping:
1) On bootstrapping time, user JWT will not be sent as a part of
request, so there is no mechanism to send a valid API call to Mainflux.
2) This way, Bootstrap service will be in sync with Mainlux: each Thing
existing in BS will also be in Mainflux.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add Thing update

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove API key from BS service

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Improve channels update algorithm

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor code

Remove unused fields, comment code and simplfy some method signatures.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove Identity Provider and use gRPC

Update dependencies

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add external auth key

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update BS config reader

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update docker-compose

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update env variable read

Add MQTT password to bootstrap response.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update response fields and tags

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove status check

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Enable BS of active Things

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add NewThing state

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Rename Status to State

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update README.md

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add filterng

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update List endpoint

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix Database query

Remove copyright headers.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add filter type

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Gateway provisioning (1.d)

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update self-bootstrapping feature

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add mocks

Update dependencies to the newest Mainflux version.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add thing service tests

Mocks fix.
Some of the service code intentionally left untested due to possible
changes in future.
Fix copyright headers and update Mainflux and other dependencies.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use name "Config" instead of "Thing"

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor code

Remove commented code.
Fix typo.
Remove unused exported error.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Simplify service tests

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove Assign method

Raise test coverage.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update database schema

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Store unknown bootstrap attempts

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update unknown bootstrap handling

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update naming

Fix uses of `Thing` in DB and `api` package.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add endpoint tests

Currently, only test for adding a new Config are implemented.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add initialization of DB tests

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add DB tests

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update readme file

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add API docs

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove Mainflux from vendor

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add licence headers

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix service and endpoint tests

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Raise test coverage

Remove unused repsonse type.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update build and deployment

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update API docs

Fix typo.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update imports formatting

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Make state response empty

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Raise test coverage

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update API docs

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update readme file

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use uuid as a primary key

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use Mainflux ID

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove `Created` state.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Move State to separate file

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add Things prefix

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update API and API docs

Be consistent in API naming and add some useful comments.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor repository implementation

Cleanup code, make it more readable. Fix missing drop in migrations.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use "cfg" insted of "thing"

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix tests

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update tables names

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2019-01-09 15:42:23 +01:00
Aleksandar Novaković 8dbe2f4066 Add dedicated env vars for event sourcing (#536)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2019-01-08 22:08:24 +01:00
Drasko DRASKOVIC 72fa9db939 NOISSUE - Fix docs (#535)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2019-01-08 21:15:00 +01:00
Manuel Imperiale 7a3d7fa8ec Add lora doc to getting-started.md (#529)
* Add lora doc to getting-started.md

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Typo fix 

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix doc

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add missing components doc to architecture.md (#531)

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix doc

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2019-01-08 12:25:13 +01:00
Aleksandar Novaković ea1993665d MF-483 - Enable channels and devices corresponding lists in backend (#520)
* Add list channels by thing id endpoint

Add list channels by thing id endpoint to things service. Add
pagination format and logic.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add fetch channels by thing endpoint

Add fetch channels by thing endpoint with pagination.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update list endpoints to contain pagination

Update list endpoints to contain pagination metadata.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add tests for two new endpoints

Add tests for two new endpoints and update existing ones. Also,
remove connected field from channel response.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix tests for SDK

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add SDK methods for new endpoints

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update swagger docs for things service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add error handling to http tests

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix response body in http tests

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Remove unused responses from things service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add test cases to things postgres tests

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add test cases for event sourcing

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2019-01-08 11:53:24 +01:00
Aleksandar Novaković bd00c5fa13 NOISSUE - Fix event sourcing client on LoRa adapter (#527)
* Fix event sourcing client on LoRa adapter

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix tests for things redis streams

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2019-01-07 23:54:18 -02:00
Manuel Imperiale c75aaa9d45 Add missing components doc to architecture.md (#531)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2018-12-29 22:17:35 +01:00
Aleksandar Novaković cdd2eb4bc7 Fix MQTT adapter scaling issue (#526)
MQTT adapter wasn't scalable because mq emitter wasn't setup. Now
it's working as intended. You can create two different instances
and exchange messages between their clients.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-12-27 10:44:57 +01:00
Aleksandar Novaković 584c8591ad Fix logging level in MQTT adapter (#522)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-12-21 17:45:49 +01:00
Aleksandar Novaković 643dc12408 Remove heartbeat log from MQTT broker (#521)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-12-21 16:14:54 +01:00
Manuel Imperiale 655141e941 MF-433 - Add method for reading messages from readers to SDK. (#511)
* MF-433 - Add method for reading messages from readers to SDK

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add messages read command on CLI

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix review

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2018-12-18 22:04:34 +01:00
Manuel Imperiale c8069827bb MF-506 - Fix influx & mongo readers queries to return most recent messages (#510)
* MF-506 - Fix fluxdb-reader query to return recent messages

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* MF-506 - Fix mongodb-reader query to return messages sorted by descendent time

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix readers tests

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2018-12-18 15:17:55 +01:00
Aleksandar Novaković 1e017bdbcc Fix data scheme in cassandra writer (#514)
Fix data scheme so that cassandra stores channel and thing id as
text. Support sorting by time per channel partition.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-12-17 20:41:11 +01:00
Manuel Imperiale 5f6bbf4b0a MF-419 - Use JSON for CLI commands output (#504)
* MF-419 - Use JSON for CLI commands output

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* dep ensure

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Remove accidentaly added CLI binary

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix cast

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix review

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Typo fix

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use logCreated for token command

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix review

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2018-12-16 14:01:37 -05:00
Michael Finley 49a41d909e MF-448 - Add option to connect to DB with verify-ca and verify-full (#500)
* MF-448 - Add option to connect to DB with verify-ca and verify-full

Adds the option to connect with verify-ca and verify-full
Users can now specify any extra certs and keys they may need.

Signed-off-by: MichaelFinley <Michael.Finley@target.com>

* Passing db config struct rather than seperate parameters

Also updated the tests to use the config

Signed-off-by: MichaelFinley <Michael.Finley@target.com>

* Unexporting fields apart of users config

Also added comments to the newly exported Config in things & users postgres/init.go

Signed-off-by: MichaelFinley <Michael.Finley@target.com>
2018-12-16 00:28:22 +01:00
Manuel Imperiale d1012d14c7 Add name to influx tagset (#501)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2018-12-11 08:32:10 -08:00
Drasko DRASKOVIC 8a2734d122 NOISSUE - Housekeeping. Update README. (#497)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-12-09 20:06:20 +01:00
Drasko DRASKOVIC 399cc051b3 NOISSUE - Fix CHANGELOG (#496)
* NOISSUE - Fix CHANGLEONG link

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* NOISSUE - Fix CHANGELOG

Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-12-08 21:47:19 +01:00
Drasko DRASKOVIC 179471d4d7 NOISSUE - Fix CHANGLEONG link (#495)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-12-08 20:58:28 +01:00
Drasko DRASKOVIC 607a0b22e0 Add CHANGELOG. Bump version. (#494)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-12-08 20:53:39 +01:00
Drasko DRASKOVIC f354332fa1 NOISSUE - Add changelog target to Makefile (#493)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-12-08 20:12:24 +01:00
Drasko DRASKOVIC 9725c2723d MF-486 - Add provisioning command to CLI (#487)
* MF-486 - Add provisioning command to CLI

Resolves #486

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Add error check on file open

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Add provision test to CLI

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Update error handling

Update CSV file.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix provision for new SDK

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Correct structs and sprintfs

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Check array length

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Correct sprintf. Update dep lock file.

Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-12-08 16:59:55 +01:00
Manuel Imperiale 61eed0c72d Fix lora-adapter event store handlers (#492)
Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2018-12-05 19:57:23 +01:00
Manuel Imperiale 715a662f0d NOISSUE - Add LoRa route map validation and fix LoRa messages URL (#491)
* Add route map validation and fix LoRa messages URL

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm private error comments

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix typo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add runlora command in Makefile

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2018-12-05 15:15:15 +01:00
Aleksandar Novaković b9bf63e377 MF-475 - Replace increment ID with UUID (#490)
* Update increment ID to UUID in things service

Update increment ID to UUID for things and channels in things
service and proto files. Also, update ID type from uint to string.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update increment ID to UUID in http adapter

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update increment ID to UUID in ws adapter

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update increment ID to UUID in CoAP adapter

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update increment ID to UUID in normalizer service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update increment ID to UUID in writer services

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update increment ID to UUID in reader services

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update increment ID to UUID in SDK

Update increment ID to UUID in SDK. Update id type to string.
Update tests.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update increment ID to UUID in mqtt adapter

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Remove unnecessary case from influxdb reader

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update tests in order to increase code coverage

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update lora adapter to use string ID instead of unsigned int

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-12-05 13:09:25 +01:00
Manuel Imperiale 21ab52576c MF-166 - Add lora-adapter service (#481)
* MF-166 - Add lora-adapter service (#416)

* MF-166 - Add lora-adapter service

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix NATS connexion and use credentials with gRPC

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Forward lora msgs to nats

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add make cmd and docker-compose

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Define lora conf as private

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename funcs fix nats conn

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Update README and fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm NATS sub

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* MF-166 - Add lora-adapter service  (#461)

* MF-166 - Add lora-adapter service

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix NATS connexion and use credentials with gRPC

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Forward lora msgs to nats

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add make cmd and docker-compose

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Define lora conf as private

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename funcs fix nats conn

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Update README and fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm NATS sub

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix lora server topic and logs

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix HTTP port

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* NOISSUE - Add event sourcing client to LoRa adapter (#471)

* Add event sourcing client to LoRa adapter

Signed-off-by: Aleksandar Novaković <anovakovic01@gmail.com>

* Update redis version in docker compose and update env vars

Signed-off-by: Aleksandar Novaković <anovakovic01@gmail.com>

* Add ES subscription to main LoRa function

Signed-off-by: Aleksandar Novaković <anovakovic01@gmail.com>

* Add new env vars to readme file of LoRa adapter

Signed-off-by: Aleksandar Novaković <anovakovic01@gmail.com>

* Add message acknowledgement to LoRa adapter

Signed-off-by: Aleksandar Novaković <anovakovic01@gmail.com>

* Add handling of empty values to event sourcing client (#474)

Signed-off-by: Aleksandar Novaković <anovakovic01@gmail.com>

* Add routemap and handle event sourcing

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix eventStore decoding

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Mv docker-compose in docker/addons

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix routemap and logs

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Update Gopkg.toml

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix route map and typos

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Update README

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix reviews

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2018-12-04 20:08:30 +01:00
Drasko DRASKOVIC 7db3b295be NOISSUE - Add Makefile target to clean old imgs (#485)
* NOISSUE - Add Makefile target to clean old imgs

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Simplify docker clean commands

Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-12-03 16:19:09 +01:00
Aleksandar Novaković 671c32affe MF-473 - Add metadata field to channel (#476)
* Add metadata field to channels and update things metadata

Add metadata field to channels. Update thing metadata field to be
in JSON format. Update tests..

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add metadata to channel events

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add metadata field to channel in go SDK

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-11-28 15:58:48 +01:00
Dušan Borovčanin bcadd7c47b Make CoAP ping period configurable (#469)
Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2018-11-27 14:02:30 +01:00
Aleksandar Novaković 59c5e350e1 Add nginx ingress config to k8s services (#472)
Update README in accordance to latest changes. Add gRPC load
balancing setup. Add healthcheck to k8s setup.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-11-27 12:56:36 +01:00
Dušan Borovčanin 6cd11536a0 Add CoAP section in getting-started (#468)
Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2018-11-27 12:46:18 +01:00
Ivan Milošević 4f01c0aa7a NOISSUE - Move CLI documentation from getting started guide to separate page (#470)
* Move CLI documentation from getting started guide to seperate page

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Fix cli usage examples in readme and docs

Signed-off-by: Ivan Milošević <iva@blokovi.com>
2018-11-27 08:40:56 +01:00
Ivan Milošević 15667120b7 NOISSUE - Update Getting Started doc with CLI usage (#465)
* Update "getting started" docs with CLI usage

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Update CLI description

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Update CLI docs with help flag

Signed-off-by: Ivan Milošević <iva@blokovi.com>
2018-11-26 12:50:32 +01:00
Dušan Borovčanin 9ede8c70b9 Update CoAP docs with URL example (#463)
Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2018-11-22 18:27:01 +01:00
Aleksandar Novaković a680fa35d2 MF-447 - Add event sourcing to things service (#460)
* Add event sourcing middleware for things service

Add event sourcing for write operations over things and channels.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add tests for event sourcing middleware

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update things in tests to contain metadata field

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-11-20 19:46:33 +01:00
Aleksandar Novaković 8ddd78c818 Add TLS support to CoAP adapter and all readers (#459)
* Add TLS support to CoAP adapter and all readers

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add new env vars to README files

Add tls env vars to README files for CoAP adapter, WS adapter
and all readers.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update all services to exit on invalid TLS env var value

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-11-20 12:52:28 +01:00
Ivan Milošević ee262b9647 MF-417 - Implement SDK tests (#438)
* Add SKD tests for creating channel

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add SDK tests for Channel function

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add list channels over SDK tests

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add SDK tests for updating channel

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add SDK tests for deleting channel

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add users service SDK tests

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* SDK things tests

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add SDK test for connecting and disconnecting things from channel

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* testing SDK sending messages

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* add tests for SDK func SetContentType

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* add all test cases for sending messages

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add handling StatusBadRequest when deleting thing over SDK

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Update error responses when deleting channel and thing

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Removed unused Unauthorized response when creating user

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* update testing CreateChannel, tests if response is some string

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add bad request case in testing CreateToken

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Remove response error conflict from things service

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Fix definition of sdk.ErrFailedDisconnect and return error in test cases for disconnecting things

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add handling errors and formatting code

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Defined new ErrFailedPublish error in SDK

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add SDK test for version

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Delete unused http response status in sdk.DeleteChannel

Signed-off-by: Ivan Milošević <iva@blokovi.com>
2018-11-19 21:27:01 +01:00
Dušan Borovčanin 0379eb6c0f MF-454 - Use message Time field as a time for InfluxDB points (#455)
* Use message time as Point time in InfluxDB

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use actual message time

Update all Reader and Writer services to use time from the message
instead of time given from the corrseponding Writer service.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove message time check

Messages time less than 2**28 represent time relative to the current time so Writer used to convert this to the correct value, i.e. msg.Time += time.Now(). However, this step is optional and should really be a part of the app on top of Mainflux or could be introduced with minor changes in Normalizer, Reader or Writer services, so there is no need for this to be supported out of box.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use channel and publisher as tag keys

Move all the other Message fields to the field keys.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2018-11-18 16:42:39 +01:00
Dušan Borovčanin bf299a0143 NOISSUE - Add .dockerignore to project root (#457)
* Add .dockerignore file to project root

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Cleanup project root

Update run script and remove unused files from project root.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove .dockerignore form docker dir

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2018-11-13 22:31:13 +01:00
Aleksandar Novaković ba4b166c96 Update docker-compose so that every service has debug log level (#453)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-11-08 21:22:59 +01:00
Aleksandar Novaković c058cf95e4 NOISSUE - Add TLS flag for Mainflux services (#452)
* Add TLS flag that will turn TLS on or off

Add TLS flag to things, http, ws and mqtt service.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update readme files with new env vars

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-11-08 19:29:58 +01:00
Nikola Marčetić bd3c24ed83 Version updated (#450)
* Version updated

Signed-off-by: nmarcetic <n.marcetic86@gmail.com>

* Updated load test version

Signed-off-by: nmarcetic <n.marcetic86@gmail.com>
2018-11-08 10:44:12 +01:00
Michael Finley eb38c36f80 MF-448 - Option for Postgres SSL Mode (#449)
* MF-448 - Option for Postgres SSL Mode

Adds an option to choose the ssl mode when connecting to postgres.
Only supporting disable or require for now with verify-ca and verify-full to come after more discussion.

Signed-off-by: Michael Finley <Michael.Finley@target.com>

* Changing package name back

Accidentally changed the package name so reverting that!

Signed-off-by: Michael Finley <Michael.Finley@target.com>

* Adding section in getting-started for securing pgsql connections

Signed-off-by: Michael Finley <Michael.Finley@target.com>
2018-11-07 21:54:51 +01:00
Dušan Borovčanin ec0a148ad4 MF-443 Update project dependencies (#444)
* Update project dependencies

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update InfluxDB and Cassandra versions

Update InfluxDB and Cassandra versions in docker-compose file as well as
images version in tests.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update gRPC version

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Make name first value of constraints

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2018-11-07 10:18:57 +01:00
nwest1 260df3ae40 MF-426 - Add optional MF_CA_CERTS env variable to allow GRPC client to use TLS certs (#430)
* MF-426-Add optional MF_CA_CERTS env variable to allow GRPC client to use TLS certs

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* enable things client to be configured with a ca cert path

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* MF_CA_CERTS docs for http adapter and things service, additional logging and improved error handling when setting up TLS gRPC client

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* refactor things connect to separate function

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* getting-started updates, corrected things env variable

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* clarifying ca certs default functionality

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* configuring tls termination at service endpoint

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* enable TLS configuration for users and things

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* nginx forwarding

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* go imports

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* bad logging change

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* naming specifically to the http adapter component

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* updated tls keys, slightly different grpc configuration

set localhost, users and things as subject alternative names

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* log message consistency

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* readme updates related to server ssl configuration

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* Trying to resolve confilcts

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* removing conflicting lines from docker-compose

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* adding back http-adapter configuration

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* slight readme update

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>

* readme tweaks

Signed-off-by: nwest1 <nwest1@users.noreply.github.com>
2018-11-06 21:09:17 +01:00
Dušan Borovčanin dfa766e013 MF-407 - Values of zero are being omitted (#434)
* Fix empty protobuf values

Update Normalizer service and .proto files. Reader and Writer services
needs to be updated due to message format change.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update HTTP adapter to use gogo protobuf

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update Reader services API

Update API or Reader services to match Message changes due to switching to gogo/proto.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update InfluxDB services

Update InfluxDB Reader and Writer services to match new Message format.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update MongoDB services

Update MongoDB Reader and Writer services to match new message format.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update Cassandra services

Update Cassandra Reader and Writer service to match new Message format.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix InfluxDB Reader test

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update Makefile and docs accordingly

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix possible data race in InfluxDB writer

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update InfluxDB Writer tests

Raise test coverage.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix InfluxDB reader

Fix wrong ValueSum readings. Upadete tests and raise coverage.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update MongoDB services tests

Raise test coverage for MongoDB Reader and Writer services.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update Readers API tests

Raise test coverage.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Raise test coverage

Update Cassandra Reader and Writer services tests.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use gogo/protobuf in CoAP adapter

Add gogo/protobuf to Gopkg.toml and update dependencies.
Update Dockerfile to run `make proto`.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update Cassandra Reader tests

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor code

Improve code style and comments to improve readability.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Rename Sum to SumValue

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Rename Values to Value

Since message contains only single value (or possibly no value at all),
`Values` name could be misleading. Rename simple double value from
`Value` to `FloatValue` accordingly.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix InfluxDB Reader logging

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Replace exclusive if statements with switch-case

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update Cassandra services tests

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2018-11-05 19:18:51 +01:00
Jovan Kostovski 8c0c994385 Expose the InfluxDB and Cassandra ports to host (#441)
* Expose the InfluxDB and Cassandra ports to host

Expose the InfluxDB's HTTP API port 8086 and  Cassandra's CQL port 9042 from the containers
to the host machine, in order to enable easier exploration of the data stored in the databases
from the host machine and development of apps which connect and use the data stored in the databases.

Signed-off-by: Jovan Kostovski <chombium@gmail.com>

* Port exposing cleanup in docker-compose.yml files

There were some doubled configurations for port exposing with the expose and ports directives in the docker-compose.yml files which had to be cleaned.

Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-11-02 09:58:27 +01:00
hykuan 694d67ada2 fix: add missing ThingsURL back (#440)
Signed-off-by: hykuan <hykuan1126@gmail.com>
2018-11-01 10:20:43 +01:00
Dušan Borovčanin d6755e4a72 MF-374 - Bring back CoAP adapter (#413)
* Bring old CoAP code back

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix channel ID formatting due to type change

Uncomment error handling for authorization.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update CoAP adapter docs

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add copyright headers

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove redundant type declaration

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add CoAP adapter to the list of services

Add CoAp adapter in Makefile services list and fix corresponding documentation.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor CoAP code

Merge multipe `const` block int single and declare consts before vars.
Un-export notFound handler since there is no need to export it.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update http version endpoint

This separates CoAP and HTTP APIs.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor CoAP POST method handling

This PR is a part of CoAP adapter refactoring that will simplify adapter implementation.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor CoAP adapter

Change CoAP message handling to simplify adapter implementation.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add backoff timeout for server ping to client

Update CoAP adapter to provide subset of necessary features from
protocol specification.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Fix leaking locked goroutine

In case of the stopped ticker, its channel is NOT closed, so pinging might be left stuck waiting for the stopped ticker to send a notification.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Format code

Use more meaningful name for Handlers map.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use and stop ticker from the same goroutine

Stop handler Ticker from ping goroutine rather than the cancel goroutine.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Check if subscription already exists in put method

Fix potential leak of handlers providing check inside of put method.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use MessageID as Observe option

Since MessageID satisfies observe option behaviour, use Message ID
instead of local timestamp. Remove Thicker from handler and use it on
transport layer.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Use name Observer insted of Handler

Name `Observer` is used in protocol specification, so this naming makes
code more self-documenting.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add CoAP adapter to docker-compose.yml

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add copyright headers

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Remove unused constants

Fix service name in startup log message.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Add metrics endpoint

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Refactor code

Config fields from main.go should not be exported; minor style changes.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>

* Update authorization URI-Query option

Use `authorization` value in URI-Query option instead of `key`. This
mimics Authorization header in some other protocols (e.g. HTTP). Please
note that this value can be replaced with simple `auth` to save space,
due to constrained URI-Query option size.

Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
2018-10-31 18:53:25 +01:00
Drasko DRASKOVIC 9cacf5708b NOISSUE - Add info log-level for development (#427)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-10-24 17:13:29 +02:00
Aleksandar Novaković ad5c66fad2 NOISSUE - Refactor SDK and things service (#420)
* Refactor Mainflux go SDK

Add structures instead of string parameters. Add offset and limit
parameters to things and channels methods. Add better configuration
support.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add new public errors with better error handling

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update SDK to use uint instread of string id

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update cli to use new SDK API

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Remove TLS termination from nginx configuration

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update SDK documentation and structures

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Refactor things service

Decouple HTTP layer from business logic. Remove ID number validation
check. Remove models from HTTP requests and responses.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Reformat tests for things service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Increase test coverage for things service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-10-24 10:21:03 +01:00
Ivan Milošević d89e91143b Fix NATS install instruction (#423)
Signed-off-by: Ivan Milošević <iva@blokovi.com>
2018-10-21 22:47:53 +02:00
Sava R 96bcc4cff4 Update NATS docker image to newer version in docker-compose (#418)
Signed-off-by: Sava Radoš <srados@bitsfactory.com>
2018-10-15 11:05:14 +02:00
Drasko DRASKOVIC a8eb7ae039 MF-354 - Add Go SDK (#357)
* MF-354 - Add Go SDK

This PR adds Go SDK. It also refactors `cli` to use new SDK.

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Use http consts. Add doc.

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Insline const

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Add initial SDK tests

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Fix SDK to return values (not HTTP rsp)

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Fix CLI and test

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* fix typos, add header

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Fix doc

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Fix doc, add comment

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Inline error checks

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Fix typos

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Inline errs

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Fix typo

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Change fnc parameter name

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Rename getters to Go standard

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Use struct and interface

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Simplify sdk struct

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Fix README

Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-10-14 16:44:21 +02:00
Ivan Milošević 6eade2c1d0 NOISSUE - Add pull request template (#414)
* Add pull request template

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Fix formatting in docs

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Fix link for contributing guidelines

Signed-off-by: Ivan Milošević <iva@blokovi.com>
2018-10-04 10:04:58 +02:00
Dušan Borovčanin 148c1aca0a MF-404 - InfluxDB batch size and timeout zero values (#411)
* Add zero value check for batch size and timeout

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update InfluxDB writer tests

Test creation of InfluxDB writer with invalid (zero) batch size and timeout.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix config load

Loading batch size and timeout from env variables were missing in loadConfigs method.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-10-01 17:36:53 +02:00
Aleksandar Novaković 70517bd907 Update mqtt to mqtt communication to support full protocol (#408)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-09-30 19:00:21 +02:00
Ivan Milošević d5f0d7d225 MF-384 - Add test for Redis cache (#405)
* Add test

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add test

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add test

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add test

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add test

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* MF-384 - Add test for Redis cache

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add copyright headers

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Fix copyright header

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Fix test setup

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* handling errors and put test cases in maps instead of slices

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add test case if thing already exists

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Fix TestThingSave to use require instead of assert

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Add test case for removing non-existing thing from cache

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Use table of test cases for Remove and test Connect for every case

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Use table of test cases for Save

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Test cases in slice instead of map for testing Remove

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Make test-cases independent, use asserts instead of requires

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Use slice and map where appropriate

Signed-off-by: Ivan Milošević <iva@blokovi.com>
2018-09-30 02:03:00 +02:00
Michael Finley b20a846071 Fixing level_test.go (#406)
Signed-off-by: Michael Finley <Michael.Finley@target.com>
2018-09-26 20:22:26 +02:00
Michael Finley 6600d26ef1 MF-379 - Log Level Option (#402)
* Adding an option to select log level

Signed-off-by: Michael Finley <Michael.Finley@target.com>

* making error default log level

Signed-off-by: Michael Finley <Michael.Finley@target.com>

* removing go-kit/levels and expanding levels wrapper

Signed-off-by: Michael Finley <Michael.Finley@target.com>

* refactoring test cases and using log.fatal

Signed-off-by: Michael Finley <Michael.Finley@target.com>

* logger.new no longer accpets enum and now accepts string for level

Signed-off-by: Michael Finley <Michael.Finley@target.com>

* level_test.go refactor to compare error

Signed-off-by: Michael Finley <Michael.Finley@target.com>

* Updating the ws README

Signed-off-by: Michael Finley <Michael.Finley@target.com>

* Adding log level for mqtt Adapter

Signed-off-by: Michael Finley <Michael.Finley@target.com>
2018-09-26 18:58:51 +02:00
Dušan Borovčanin 3de34062db NOISSUE - Influxdb batch add (#394)
* Add batch of streams

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add env variables for batch point setup

Fix InfluxDB reader tests.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update Compose and Kubernetes config

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update env variables

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update docs

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Remove unused const

Refactor code and docs.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Handle NewBatchPoints error

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Reduce wait time in  batch save test

Fix typos.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update tests

Use single test with multiple cases to test single point save as well as
a batch of points.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add an explanation for not resetting ticker

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-09-23 01:53:03 +02:00
Ivan Milošević 8e9d7b4fdb MF-389 - Add documentation for Writers and Readers (#398)
* MF-389 - Add documentation for Writers and Readers

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* MF-389 - Update readme files for readers and writers

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* MF-389 - Update mongodb-reader docker-compose.yml

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Fix links formatting in docs

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Updated reader and writer docs

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Fix formatting

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Update Grafana access

Signed-off-by: Ivan Milošević <iva@blokovi.com>
2018-09-20 15:32:50 +02:00
Dušan Borovčanin 5757380a63 NOISSUE - Dev dockers (#392)
* Add development Docker images build

Add Dockerfile.dev and target in the Makefile to provide much faster image
building for `development purposes`.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Build single service in dev mode

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update docs

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix Makefile

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix dev-guide

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-09-19 20:20:12 +02:00
Jovan Kostovski c944205080 dev guide spelling and formatting fixes (#393)
Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-09-17 21:32:13 +02:00
Drasko DRASKOVIC d825174ccb NOISSUE - Separate docker-push into function (#386)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-09-13 15:40:40 +02:00
Aleksandar Novaković 6408b8a9d3 Fix NATS subscription bug in writers (#391)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-09-13 10:06:34 +02:00
Drasko DRASKOVIC 845b69a4ac Bump version to 0.5.1 (#385)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-09-04 23:32:04 +02:00
Aleksandar Novaković 0c77d84176 MF-382 - Improve performance by adding Redis cache for message auth (#383)
* Add redis caching for thing and channel connections

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix authorization caching flow

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update things documentation

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-09-04 22:19:43 +02:00
Sava R 970c1c86ca Fix topic to channel parsing (#381)
Signed-off-by: Sava Radoš <srados@bitsfactory.com>
2018-09-04 14:07:51 +02:00
Aleksandar Novaković 902630fb14 Fix type and name switching in connected things (#376)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-08-29 14:28:56 +02:00
Drasko DRASKOVIC 4ca443038d Update MAINTAINERS 2018-08-28 22:42:47 +02:00
Drasko DRASKOVIC 47c56cbac9 Update MAINTAINERS (#373)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-08-28 22:41:25 +02:00
Sava 0fd0e6a13f Initial user, device, channel provisioning script used to speed up testing on a fresh Mainflux installation. Accepts email, password, device name, channel name as arguments. (#369)
Signed-off-by: Sava Radoš <sava.rados@mainflux.com>
2018-08-27 20:51:17 +02:00
Jovan Kostovski 8a86593dc3 NOISSUE - Makefile test target, CREATE TABLE and dev guide enhancements (#359)
* Fixes #337

Signed-off-by: Jovan Kostovski <chombium@gmail.com>

* added create table if not exists

Signed-off-by: Jovan Kostovski <chombium@gmail.com>

* added test target in sync with Semaphore CI

Signed-off-by: Jovan Kostovski <chombium@gmail.com>

* added warning about the possible data loss

Signed-off-by: Jovan Kostovski <chombium@gmail.com>
2018-08-27 12:17:41 +02:00
Ivan Milošević effade00aa MF-325 - Add SPDX license and copyright headers (#362)
* MF-325 - Add SPDX license and copyright headers

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* MF-325 - Add SPDX license and copyright headers

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* MF-325 - Add SPDX license and copyright headers

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* MF-325 - Add SPDX license and copyright headers

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* MF-325 - Change mainflux version from 0.4.0 to 0.5.0

Signed-off-by: Ivan Milošević <iva@blokovi.com>
2018-08-26 13:15:48 +02:00
Sava 8edf6907bc Change project retrieval procedure from git clone to go get (#366)
Signed-off-by: Sava Radoš <sava.rados@mainflux.com>
2018-08-25 14:39:30 +02:00
Dušan Borovčanin 507bd2ee84 MF-311 - Implement basic InfluxDB reader (#365)
* Add InfluxDB reader

Summary:
- Add basic reader features
- Update Makefile

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Raise test coverage

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update README.md and docker composition

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix docker-compose.yml

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Remove exposed ports

Ports are already exposed by mapping, so no need to explicity expose them.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-08-25 12:48:03 +02:00
Sava 86f0d8e0d6 MF-363 - Add compiled .proto files (#364)
* Add compiled .proto files

Signed-off-by: Sava Radoš <sava.rados@mainflux.com>

* Remove proto as prerequisite for build services target

Signed-off-by: Sava Radoš <sava.rados@mainflux.com>
2018-08-24 15:33:40 +02:00
Aleksandar Novaković 6461761985 Add logging middleware to normalizer service (#360)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-08-21 10:15:50 +02:00
Paul RATHGEB ec03d877f0 Declare docker_mainflux-base-net as external and add it to all services (#361)
Signed-off-by: Paul RATHGEB <paul.rathgeb@skynet.be>
2018-08-20 22:00:45 +02:00
Ivan Milošević 4e137a3214 MF-343 - Thing Model: rename the Payload Field to Metadata (#358)
* Rename Payload field to Metadata

Rename Payload field of Thing to Metadata.

Signed-off-by: Ivan Milošević <iva@blokovi.com>

* Revert JSON payload rename in docs

Signed-off-by: Ivan Milošević <iva@blokovi.com>
2018-08-17 16:20:35 +02:00
Drasko DRASKOVIC af281da430 NOISSUE - Add insecure param to cli (#356)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-08-17 04:58:09 +02:00
Drasko DRASKOVIC 1683d17830 Add some make command info (#353)
Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-08-16 21:51:49 +02:00
Drasko DRASKOVIC 7e83b2d612 NOISSUE - Add system run script (#350)
* NOISSUE - Add system run script

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Add MQTT

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Add MQTT module install to Makefile. Fix warnings.

Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-08-16 19:52:43 +02:00
Drasko DRASKOVIC 312d30fd69 NOISSUE - Add Development Guide doc (#351)
* NOISSUE - Add Development Guide doc

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Add MQTT info

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Fix typos

Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-08-16 19:44:11 +02:00
Drasko DRASKOVIC 2c988c19b0 NOISSUE - Rename bashflux to cli and fix cert path (#349)
* NOISSUE - rename bashflux to cli and fix cert path

Signed-off-by: drasko <drasko.draskovic@gmail.com>

* Fix pkg name

Signed-off-by: drasko <drasko.draskovic@gmail.com>
2018-08-16 13:41:27 +02:00
Paul RATHGEB 455fedf075 Fix the influxdb hostname. Fix #346 (#348)
Signed-off-by: Paul RATHGEB <paul.rathgeb@skynet.be>
2018-08-16 04:06:25 +02:00
Aleksandar Novaković 58cdf2cddc MF-312 - Implement basic MongoDB reader (#344)
* Add mongodb reader service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add tests for mongodb reader service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add documentation for mongodb reader service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix test function name

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update comment in docker-compose for mongodb-reader service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-08-08 13:38:34 +02:00
Aleksandar Novaković 6d48446c16 MF-313 - Implement basic Cassandra reader (#331)
* Fix logger message in http service

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Inline query and error handling in cassandra writer

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix comments and import statement in writer interface

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add reader common interface and shared HTTP API

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add Cassandra reader implementation

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add tests for cassandra reader

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add swagger doc and readme for readers

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update make file

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add docker-compose configuration for cassandra reader

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add readme file to cassandra reader

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-08-06 17:06:55 +02:00
Ivan Milošević 29d146636a Change favicon on Dashflux (#341)
Signed-off-by: Ivan Milosevic <iva@blokovi.com>
2018-08-01 23:11:40 +02:00
Marco Ferragina f3f14e803d Improved getting started docs (#339)
* Improved getting started docs

Better explaination of how to send messages to a channel using websockets.
Added a basic nodejs example

Signed-off-by: Marco Ferragina <marco.ferragina@gmail.com>

* Added leading slash

Signed-off-by: Marco Ferragina <marco.ferragina@gmail.com>

* added some comments to example code in docs

Signed-off-by: Marco Ferragina <marco.ferragina@gmail.com>
2018-07-25 11:12:31 +02:00
Manuel Imperiale b090aa3e02 MF-216 - Integrate Bashflux into monorepo (#308)
* MF-216 - Integrate Bashflux into monorepo

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename clients -> things

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix FormatResLog

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Initialize Channels commands in channels.go

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Mv bashflux to cmd/ repertory

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Mv API commands files to bashflux repertory

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Initialize Users commands in users.go

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Initialize Version command in version.go

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Initialize Messages command in messages.go

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm bashflux .gitignore and mv commands to root .gitignore

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename API commands vars

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix certificates paths

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm test logs

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Remove get channels cmd without arguments

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix connect and disconnect commands

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix messages endpoint

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm useless comments and dead code

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use contentTypeSenml var

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename cmdCobra -> cmd

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm else statments

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename SetServerAddr function vars

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename conf parameters

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename main with proper name

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix Update channel comment

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix README

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm .DS_Store from .gitignore

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename hhtp_client.go -> http.go

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm DeleteAllChannels and DeleteAllThings funcs

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix BF users usage log

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Improve bashflux logs

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Share types in funcs

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Typo fix

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix BF version cmd

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Set certs paths via env variables

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix package

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rm not direct dependencies from Gopkg.toml

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix README

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Add def prefix to certificates paths

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* User thiings service for version cmd

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Rename GetReqResp -> SendRequest

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix version help

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Return directly the pointer in NewVersionCmd

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Define endpoints names as consts and be consistent with naming

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Use Spintf for string concatenation

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* typo fix

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix README

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix version endpoint

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>

* Fix serverAddr

Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
2018-07-11 13:53:37 +02:00
Dušan Borovčanin 0b954847fd MF-334 - Correct normalizer k8 port (#335)
* Fix k8s normalizer deployment port

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Remove targert port

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-07-02 14:53:16 +02:00
Dušan Borovčanin 44f5a7241f NOISSUE - Expose SenML subject from Normalizer service (#330)
* Expose SenML subject from Normalizer service

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Export shared NATS subjects

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-06-19 00:37:04 +02:00
Dušan Borovčanin b694f08ea2 Inteface implementation files match interface name (#329)
Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-06-18 10:51:30 +02:00
Dušan Borovčanin f1537528f9 NOISSUE - Raise test coverage (#324)
* Update WS tests

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Use require in all writer tests

Refactor code.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Ignore Mainflux generated pb.go files

Ignore *.pb.go files generated by Mainflux, but don't ignore vendored generated code.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Return an exported ErrNotFound instead of the unexported one

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update mocks to match the actual behaviour

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update mocks error message

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add auth service unavailable error test

Since this error is caused by gRPC server returning codes.Internal,
this behaviour is simulated using specific token. When that token is
passed as an auth header, the mock gRPC client returns aforementioned error.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Use require package for postgres tests

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Remove redundant error checks in tests

Refactor tests.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Rename error flag token

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-06-16 02:30:46 +02:00
Aleksandar Novaković 344eed7976 NOISSUE - Add exported metrics visualization for grafana (#326)
* Add exported metrics visualization in grafana

Add graphics for request count and request latency for users, things,
http adapter and ws adapter services.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Rename grafana export file

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Move metrics configuration to metrics dir

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-06-15 17:08:49 +02:00
Aleksandar Novaković d8357b500c MF-328 - Add message persistence support for MQTT (#327)
* Add redis for message persistence

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add redis to docker composition

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix mqtt publish command in documentation

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update documentation with new environment variables

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-06-15 07:27:23 -07:00
Dušan Borovčanin 993398b8ab NOISSUE - Update Kubernetes setup (#322)
* Add InfluxDB writer services setup to k8s

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Remove commented NginX config

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-06-15 13:04:52 +02:00
Aleksandar Novaković 426f59d392 MF-235 - Add support for storing messages in Cassandra (#321)
* Add Cassandra writer implementation

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add cassandra service with version and metrics endpoints

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add test for cassandra writer

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Exclude api.go files from code coverage

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add readme file for cassandra writer

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add docker compose configuration for cassandra writer

Add README file. Add docker compose configuration.

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add gocql as project dependency

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix init script for cassandra in docker-compose

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add testifies require subpackage

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-06-08 14:25:55 +02:00
Darko Draskovic 2b393ad50f MF-237 - Add support for storing messages in MongoDB (#307)
* Add mongodb-writer

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add official mongodb driver

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Move Connect to main.go

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Remove bson.NewDoc and write msg directly in db

Signed-off-by: Darko Draskovic <darko.draskovic@gmail.com>

* Add MongoDB writer tests

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update README.md

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add mongodb services compose to addons dir

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update docs

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update docs and tests

Refactor code.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Expose MetricsMiddleware to align writers with other services

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add logging middleware

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update load tests version

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-06-01 15:50:23 +02:00
Dušan Borovčanin 9d7f4544c7 MF-296 - Create docker-compose with InfluxDB and Grafana (#306)
* Add docker-compose

Update README.md accordingly.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Move writer compose files to root docker dir

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Remove Point from InfluxDB writer config

Update docs.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-06-01 11:20:44 +02:00
Aleksandar Novaković 781d358f38 Add app prefix to dashflux (#320)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-31 17:24:23 +02:00
Dušan Borovčanin 7b6030d8ad NOISSUE - Rename path veriables in InfluxDB wirter (#315)
* Rename path variables

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update README.md

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix README.md deployment env variables

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Rename InfluxDB point environment variable

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-05-31 12:28:00 +02:00
Aleksandar Novaković 89e69d132a Fix MQTT client id bug (#319)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-30 13:48:40 +02:00
Aleksandar Novaković 7590578745 Change clients to things in documentation (#318)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-29 16:00:18 +02:00
Aleksandar Novaković 97c2d9c30a NOISSUE - Update dashflux port on kubernetes configuration (#317)
* Update dashflux port on kubernetes configuration

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Remove target port from dashflux config

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-29 10:47:27 +02:00
Aleksandar Novaković c38a032fa5 Add version endpoint to nginx configuration (#316)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-28 19:20:17 +02:00
Aleksandar Novaković 1d75268ffa NOISSUE - Fix channel closing bug in WebSocket adapter (#309)
* Remove unnecessary error checks in ws adapter

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Fix WebSocket adapter channel closing bug

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-28 12:47:20 +02:00
Aleksandar Novaković ef3627f4ee NOISSUE - Add test suite for WebSocket adapter (#299)
* Add load test for WebSocket adapter

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add new test suite to README file

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-25 11:53:45 +02:00
Dušan Borovčanin 808ac50792 MF-282 - Rename clients to things in Dashflux (#305)
* Rename services, stores, environments and models

Rename services, stores and models to match naming change from `clients` to `things`.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Replace all occurrences of clients with things

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update docker-compose and nginx.conf

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Rename `clients service` to `things service`

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Rename function and veriable names

Remove other occurences of `clients` word/prefix/sufix and replace it with `things`.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix error sending id

Id field must not be sent in POST request in order to create a new thing.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Disable thing type edit

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Export location header

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update docker config to use port 3000

Update docker-compose.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Replace id check with bool variable

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Rename dialogs

Fix bug when creating channel with no connected things.

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
2018-05-24 15:26:03 +02:00
Aleksandar Novaković 4c8aeb395c Fix WebSocket mock (#300)
Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-23 23:33:15 +02:00
Aleksandar Novaković 64e71edf95 NOISSUE - Add mutex to WebSocket service mock (#294) 2018-05-21 23:12:21 +02:00
Dušan Borovčanin c5a40aeffa NOISSUE - Remove redundant error check (#292) 2018-05-21 20:11:04 +02:00
Aleksandar Novaković 3d134d52a6 NOISSUE - Add MQTT to kubernetes nginx (#293)
* Fix nginx configuration for kubernetes

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Add dashflux to nginx configuration

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update kubernetes documentation

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>

* Update soft links paths to relative

Signed-off-by: Aleksandar Novakovic <aleksandar.novakovic@mainflux.com>
2018-05-21 18:18:13 +02:00
Dušan Borovčanin f0cbc47ee6 MF-236 - Add support for storing messages in InfluxDB (#283) 2018-05-21 16:28:52 +02:00
Dejan Mijić e0e2b2aad0 MF-269 - Replace UUID with BIGSERIAL as primary key (#291) 2018-05-21 12:51:46 +02:00
Aleksandar Novaković bbf2d5560c MF-165 - Add gRPC authorization calls to MQTT adapter (#285) 2018-05-20 09:29:19 +02:00
Dušan Borovčanin 69bfb8668e NOISSUE - Fix potential race in gRPC tests (#290) 2018-05-18 13:27:21 +02:00
Aleksandar Novaković 7043dea81a NOISSUE - Add Identify gRPC method (#288) 2018-05-17 20:17:02 +02:00
Govind KP e04d0aeaff NOISSUE - Drop $PWD from docker-compose (#286) 2018-05-17 14:38:16 +02:00
Dejan Mijić 9c6904d9b3 NOISSUE - Update load tests (#287) 2018-05-17 14:21:08 +02:00
Dejan Mijić 38a107919a MF-268 - Replace JWTs with UUIDs as thing's access keys (#284) 2018-05-16 14:28:41 +02:00
Dejan Mijić 48484210c2 NOISSUE - Rename clients to things (#281) 2018-05-15 17:13:09 +02:00
Aleksandar Novaković ceb630c782 NOISSUE - Emit non-SenML messages (#279) 2018-05-14 22:15:12 +02:00
Dušan Borovčanin 570ebc1c72 NOISSUE - Sync docker compose service ports with k8s settings (#278) 2018-05-14 17:21:06 +02:00
Dušan Borovčanin 50e14f0c65 NOISSUE - Add CoAP adapter version endpoint (#277) 2018-05-14 13:21:49 +02:00
Dušan Borovčanin d2bce84c06 NOISSUE - Update Docker compose (#274) 2018-05-14 13:11:29 +02:00
Aleksandar Novaković 89aa9603ec NOISSUE - Replace manager with users and clients services (#276) 2018-05-14 12:18:01 +02:00
Aleksandar Novaković 2ae581368f NOISSUE - Add WebSocket and CoAP services to kubernetes config (#275) 2018-05-14 12:13:04 +02:00
Aleksandar Novaković 99a3ec5d2d NOISSUE - Fix kubernetes configuration (#273) 2018-05-11 16:41:25 +02:00
Dušan Borovčanin 89a0fa3e9c NOISSUE - Handle authorization errors other than unauthorize (#264) 2018-05-11 16:37:32 +02:00
Dejan Mijić 1bc4dc9575 NOISSUE - Add MQTT build steps into Makefile (#267) 2018-05-11 01:13:33 +02:00
Dejan Mijić bca3d0774f MF-201 - Replace ORM with SQL (#265) 2018-05-11 01:00:10 +02:00
Aleksandar Novaković 816c172823 MF-164 - Split manager service (#266) 2018-05-10 23:53:25 +02:00
Dušan Borovčanin 6a361209c8 NOISSUE - Automate Dashflux container deployment (#261) 2018-05-10 10:39:48 +02:00
Dušan Borovčanin 27e1be23f5 NOISSUE - Integrate MQTT adapter to monorepo (#260) 2018-05-09 14:22:03 +02:00
Dušan Borovčanin c9bf245010 NOISSUE - Update CoAP docs (#257) 2018-05-09 12:56:20 +02:00
Dušan Borovčanin ff30957614 MF-244 - Integrate Dashflux (#258) 2018-05-09 12:32:49 +02:00
Dušan Borovčanin db6165aa6b MF-168 - Refactor CoAP adapter (#186) 2018-05-08 16:00:25 +02:00
Aleksandar Novaković c088e664c3 NOISSUE - Fix run and compile commands in README files (#249) 2018-05-02 09:41:21 +02:00
Dejan Mijić d499068e43 Update protobuf to 1.1.0 (#251) 2018-05-02 08:49:39 +02:00
Dušan Borovčanin 38d4c3fcea MF-136 - Setup staging and test environments (#226) 2018-04-30 13:32:38 +02:00
Aleksandar Novaković aea7db14b7 NOISSUE - Raise code coverage in ws adapter (#242)
* Fix failed subscription handling in ws adapter

Fix unsubscribing bug in ws adapter.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Add subscription fail and publish fail test cases

Update mock implementation to receive publish error in order to
support these test cases.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update mainflux version to 0.2.3

Update project version and load tests version to 0.2.3.

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Update version endpoint response format

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-24 13:56:13 +02:00
Aleksandar Novaković 58f3c735a0 NOISSUE - Fix Content-Type in examples (#243)
* Update content-type header in load tests

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>

* Remove charset from content type header in documentation

Signed-off-by: Aleksandar Novakovic <anovakovic01@gmail.com>
2018-04-24 13:42:36 +02:00
Dušan Borovčanin 789963447a Add service name to version response (#241)
Signed-off-by: Dušan Borovčanin <borovcanindusan1@gmail.com>
2018-04-24 13:39:15 +02:00
1754 changed files with 348980 additions and 38507 deletions
+10
View File
@@ -0,0 +1,10 @@
.git
.github
build
ui
docker
docs
k8s
load-test
metrics
scripts
+15
View File
@@ -0,0 +1,15 @@
Pull request title should be `MF-XXX - description` or `NOISSUE - description` where XXX is ID of issue that this PR relate to.
Please review the [CONTRIBUTING.md](./CONTRIBUTING.md) file for detailed contributing guidelines.
### What does this do?
### Which issue(s) does this PR fix/relate to?
Put here `Resolves #XXX` to auto-close the issue that your PR fixes (if such)
### List any changes that modify/break current functionality
### Have you included tests for your changes?
### Did you document any new/modified functionality?
### Notes
-1
View File
@@ -1,4 +1,3 @@
build
*.pb.go
site/
+207
View File
@@ -0,0 +1,207 @@
# Mainflux Changelog
## Generation
Mainflux release notes for the latest release can be obtained via:
```
make changelog
```
Otherwise, whole log in a similar format can be observed via:
```
git log --pretty=oneline --abbrev-commit
```
## 0.8.0 - 20. MAR 2019.
### Features
- MF-571 - Add Env.elm to set custom base URL (#654)
- NOISSUE Added docs about docker-compose config overriding (#653)
- MF-539 - Improve Bootstrap Service documentation (#646)
- MF-596 - Add subtopic to RawMessage (#642)
- NOISSUE - Prevent infinite loop in lora-adapter if Redis init fail (#647)
- Corrected grammar and rephrased a few sentences to read nicely (#641)
- MF-571 - Elm UI (#632)
- MF-552 - Use event sourcing to keep Bootstrap service in sync with Things service (#603)
- MF-540 - Add pagination in API responses for Bootstrap service (#575)
- MF-600 - Handle custom LoRa Server application decoder (#608)
- update docker-compose (#590)
- Update generated code (#602)
- Add generated files check (#601)
- MF-597 - Removed legacy code as not needed anymore (#598)
- NOISSUE - Added normalizer service to run script (#594)
- Changed RawMessage (#587)
- NOISSUE - fix CLI log (#581)
- MF-519 - Refine Message (#567)
- NOISSUE - Add name field for Bootstrap Config (#564)
- Fix non-SenML message routing in normalizer (#573)
- NOISSUE - Update authors list (#569)
- Update lora.md (#568)
- NOISSUE- Improve LoRa doc (#562)
- MF-551 - Add metadata fields to Bootstrap Channels (#563)
- Fix MQTT adapter by setting subscription queue (#561)
- MF-558 - Add MQTT subtopics documentation (#559)
- Fix regexp for SUB (#557)
- Simplify MQTT topipc regexp (#555)
- MF-429 -Enabled MQTT subtopic's (#554)
- Add env var for number of concurrent messages (#545)
- NOISSUE - Update doc and fix empty key bug (#544)
- MF-370 - Simplify and refine CI (#541)
- NOISSUE - Add connection commands to CLI (#542)
- NOISSUE - Refine docs (#537)
- Update licnese year (#533)
- MF-513 - Add Bootstrapping service (#524)
- Add dedicated env vars for event sourcing (#536)
- NOISSUE - Fix docs (#535)
- Add lora doc to getting-started.md (#529)
- MF-483 - Enable channels and devices corresponding lists in backend (#520)
- Add missing components doc to architecture.md (#531)
### Bugfixes
- MF-639 Split Content-Type header field on semicolon and evaluate all substrings (#644)
- MF-656 - Change bootstrap service port to 8200 (#658)
- Replace crossOrigin with relative path and fix messaging bug (#645)
- MF-579 Things & Channels returns 404 when not found or ID is malformed, not 500 (#633)
- Fix run command in dev guide (#605)
- MF-583 - Correct cmd/mongodb-reader HTTPServer log Info (#584)
- Fix Dusan Maldenovic GitHub (#570)
- Fix CLI docs (#566)
- Fix pagination response for empty page (#547)
- Fix swagger and provisioning docs (#546)
- NOISSUE - Fix event sourcing client on LoRa adapter (#527)
- Fix MQTT adapter scaling issue (#526)
- NOISSUE - Fix subtopic regex and restrict empty subtopic parts (#659)
- Fix missing css in container ui (#638)
- NOISSUE - Fix lora-adapter Object decode (#610)
- NOISSUE - Fix users logs in main.go (#577)
- NOISSUE - Fix normalizer exposed port in docker-compose (#548)
### Summary
https://github.com/mainflux/mainflux/milestone/9?closed=1
## 0.7.0 - 08. DEC 2018.
### Features
- MF-486 - Add provisioning command to CLI (#487)
- Fix lora-adapter event store handlers (#492)
- NOISSUE - Add LoRa route map validation and fix LoRa messages URL (#491)
- MF-475 - Replace increment ID with UUID (#490)
- MF-166 - Add lora-adapter service (#481)
- NOISSUE - Add Makefile target to clean old imgs (#485)
- MF-473 - Add metadata field to channel (#476)
- Make CoAP ping period configurable (#469)
- Add nginx ingress config to k8s services (#472)
- Add CoAP section in getting-started (#468)
- NOISSUE - Move CLI documentation from getting started guide to separate page (#470)
- NOISSUE - Update Getting Started doc with CLI usage (#465)
- Update CoAP docs with URL example (#463)
- MF-447 - Add event sourcing to things service (#460)
- Add TLS support to CoAP adapter and all readers (#459)
- MF-417 - Implement SDK tests (#438)
- MF-454 - Use message Time field as a time for InfluxDB points (#455)
- NOISSUE - Add .dockerignore to project root (#457)
- Update docker-compose so that every service has debug log level (#453)
- NOISSUE - Add TLS flag for Mainflux services (#452)
- MF-448 - Option for Postgres SSL Mode (#449)
- MF-443 Update project dependencies (#444)
- MF-426 - Add optional MF_CA_CERTS env variable to allow GRPC client to use TLS certs (#430)
- Expose the InfluxDB and Cassandra ports to host (#441)
- MF-374 - Bring back CoAP adapter (#413)
### Bugfixes
- gRPC Load Balancing between http-adapter and things (#387)
- MF-407 - Values of zero are being omitted (#434)
### Summary
https://github.com/mainflux/mainflux/milestone/8?closed=1
## 0.6.0 - 26. OCT 2018.
### Features
- Added Go SDK (#357)
- Updated NATS version (#412)
- Added debbug level to MFX logger (#379)
- Added Documentation for readers (#389)
- Added Redis cache to improve performance (#382)
## 0.5.1 - 05. SEP 2018.
### Features
- Improve performance by adding Redis cache (#382)
### Bugfixes
- Mixed up name and type of the things (#375)
- Fix MQTT topic (#380)
## 0.5.0 - 28. AUG 2018
### Features
- InfluxDB Reader (#311)
- Cassandra Reader (#313)
- MongoDB Reader (#344)
- MQTT Persistance via Redis (#328)
- CLI integrated into monorepo (#216)
- Normalizer logging (#333)
- WS swagger doc (#337)
- Payload renamed to Metadata (#343)
- Protobuf files added (#363)
- SPDX headers added (#325)
### Bugfixes
- Docker network for InfluxDB (#346)
- Vendor correct gRPC version (#340)
### Summary
https://github.com/mainflux/mainflux/milestone/6?closed=1
## 0.4.0 - 01. JUN 2018.
* Integrated MQTT adapter (#165 )
* Support for storing messages in MongoDB (#237)
* Support for storing messages in InfluxDB (#236)
* Use UUID PKs with auto-incremented values (#269 )
* Replaced JWT with plain string tokens in things service (#268 )
* Emit non-SenML messages (#239 )
* Support for Grafana (#296)
* Added WS Load test (#299 )
## 0.3.0 - 14. MAY 2018.
- CoAP API for message exchange (#186)
- Split `manager` service into `clients` and `users` (#266)
- Replaced ORM with raw SQL (#265)
- Setup Kubernetes (#226, #273)
- Fix docker compose (#274)
- Integrated `dashflux` into monorepo (#258)
- Integrated (*non-compatible*) `mqtt` into monorepo (#260)
## 0.2.3 - 24. APR 2018.
- Fix examples in the documentation (#243)
- Add service name in info response (#241)
- Improve code coverage in WS adapter (#242)
## 0.2.2 - 23. APR 2018.
- Setup load testing scenarios (#225)
## 0.2.1 - 22. APR 2018.
- Fixed `Content-Type` header checking (#238)
## 0.2.0 - 18. APR 2018
- Protobuf message serialization (#192)
- Websocket API for exchanging messages (#188)
- Channel & client retrieval paging (#227)
- Service instrumentation (#213)
- `go-kit` based JSON logger (#212)
- Project documentation (#218, #220)
- API tests (#211, #224)
## 0.1.2 - 18. MAR 2018.
### Bug fixes
- Fixed go lint warnings (#189)
- Compose failing startup (#185)
- Added missing service startup messages (#190)
+15 -16
View File
@@ -1,7 +1,7 @@
# Contributing to Mainflux
The following is a set of guidelines for contributing to Mainflux and its libraries, which are
hosted in the [Mainflux Organization](https://github.com/mainflux) on GitHub.
The following is a set of guidelines to contribute to Mainflux and its libraries, which are
hosted on the [Mainflux Organization](https://github.com/mainflux) on GitHub.
This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0).
By participating, you are expected to uphold this code. Please report unacceptable behavior to
@@ -9,16 +9,15 @@ By participating, you are expected to uphold this code. Please report unacceptab
## Reporting issues
Reporting issues is a great way to contribute to the project. We always appreciate a well-written,
thorough bug reports.
Reporting issues are a great way to contribute to the project. We are perpetually grateful about a well-written,
thorough bug report.
Prior to raising a new issue, check out [our issue
list](https://github.com/mainflux/mainflux/issues) to determine whether it already include the
problem you are facing.
Before raising a new issue, check [our issue
list](https://github.com/mainflux/mainflux/issues) to determine if it already contains the
problem that you are facing.
A good bug report shouldn't leave others needing to chase you up for more information. Please try to
be as detailed as possible. The following questions might serve as a template for writing a detailed
reports:
A good bug report shouldn't leave others needing to chase you for more information. Please be as detailed as possible. The following questions might serve as a template for writing a detailed
report:
- What were you trying to achieve?
- What are the expected results?
@@ -29,16 +28,16 @@ reports:
## Pull requests
Good pull requests (e.g. patches, improvements, new features) are a fantastic help. They should
remain focused in scope and avoid containing unrelated commits.
remain focused in scope and avoid unrelated commits.
**Please ask first** before embarking any significant pull request (e.g. implementing new features,
**Please ask first** before embarking on any significant pull request (e.g. implementing new features,
refactoring code etc.), otherwise you risk spending a lot of time working on something that the
maintainers might not want to merge into the project.
Please adhere to the coding conventions used throughout the project. If in doubt, consult the
[Effective Go](https://golang.org/doc/effective_go.html) style guide.
To start contributing to the project, [fork](https://help.github.com/articles/fork-a-repo/) it,
To contribute to the project, [fork](https://help.github.com/articles/fork-a-repo/) it,
clone your fork repository, and configure the remotes:
```
@@ -47,7 +46,7 @@ cd mainflux
git remote add upstream https://github.com/mainflux/mainflux.git
```
If you cloned a while ago, get the latest changes from upstream:
If your cloned repository is behind the upstream commits, then get the latest changes from upstream:
```
git checkout master
@@ -63,8 +62,8 @@ git checkout -b MF-[issue-number]
Commit your changes in logical chunks. When you are ready to commit, make sure
to write a Good Commit Message™. Consult the [Erlang's contributing guide](https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
if you're not sure what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
to group your commits into logical units of working before making them public.
if you're unsure of what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
to group your commits into logical units of work before making it public.
Note that every commit you make must be signed. By signing off your work you indicate that you
are accepting the [Developer Certificate of Origin](https://developercertificate.org/).
Generated
+513 -119
View File
@@ -3,130 +3,151 @@
[[projects]]
branch = "master"
digest = "1:6da51e5ec493ad2b44cb04129e2d0a068c8fb9bd6cb5739d199573558696bb94"
name = "github.com/Azure/go-ansiterm"
packages = [
".",
"winterm"
"winterm",
]
pruneopts = "UT"
revision = "d6e3b3328b783f23731bc4d058875b0371ff8109"
[[projects]]
digest = "1:bf42be3cb1519bf8018dfd99720b1005ee028d947124cab3ccf965da59381df6"
name = "github.com/Microsoft/go-winio"
packages = ["."]
pruneopts = "UT"
revision = "7da180ee92d8bd8bb8c37fc560e673e6557c392f"
version = "v0.4.7"
[[projects]]
branch = "master"
digest = "1:3721a10686511b80c052323423f0de17a8c06d417dbdd3b392b1578432a33aae"
name = "github.com/Nvveen/Gotty"
packages = ["."]
pruneopts = "UT"
revision = "cd527374f1e5bff4938207604a14f2e38a9cf512"
[[projects]]
digest = "1:320e7ead93de9fd2b0e59b50fd92a4d50c1f8ab455d96bc2eb083267453a9709"
name = "github.com/asaskevich/govalidator"
packages = ["."]
pruneopts = "UT"
revision = "ccb8e960c48f04d6935e72476ae4a51028f9e22f"
version = "v9"
[[projects]]
branch = "master"
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
name = "github.com/beorn7/perks"
packages = ["quantile"]
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
pruneopts = "UT"
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
digest = "1:2209584c0f7c9b68c23374e659357ab546e1b70eec2761f03280f69a8fd23d77"
name = "github.com/cenkalti/backoff"
packages = ["."]
revision = "61153c768f31ee5f130071d08fc82b85208528de"
version = "v1.1.0"
pruneopts = "UT"
revision = "2ea60e5f094469f9e65adb9cd103795b73ae743e"
version = "v2.0.0"
[[projects]]
branch = "master"
digest = "1:bb300c7f4b52deb1fcc65f6be624ae3dc289453824c2f1f184963dc41d941a68"
name = "github.com/cisco/senml"
packages = ["."]
revision = "448c9510575e1dd6f780cb10addbad998cf80418"
pruneopts = "UT"
revision = "910a55054e168c1122b905e3f8acdc5ff97cbec1"
[[projects]]
branch = "master"
digest = "1:fc8dbcc2a5de7c093e167828ebbdf551641761d2ad75431d3a167d467a264115"
name = "github.com/containerd/continuity"
packages = ["pathdriver"]
revision = "d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371"
pruneopts = "UT"
revision = "c6cef34830231743494fe2969284df7b82cc0ad0"
[[projects]]
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "UT"
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:ca71cdedcb487a361fe2588afeb3d373c0dfe21feaac229270fd6de234bd3c5e"
name = "github.com/dereulenspiegel/coap-mux"
packages = ["."]
pruneopts = "UT"
revision = "e137e310a99955be01d60b2237da89153ea40987"
[[projects]]
digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55"
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
pruneopts = "UT"
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
branch = "master"
digest = "1:a5036bc703c3994f70ced9b34e9941381aa6bd03e69a0ce538f43bf14dc8dc98"
name = "github.com/docker/docker"
packages = [
"api/types",
"api/types/blkiodev",
"api/types/container",
"api/types/filters",
"api/types/mount",
"api/types/network",
"api/types/registry",
"api/types/strslice",
"api/types/swarm",
"api/types/swarm/runtime",
"api/types/versions",
"opts",
"pkg/archive",
"pkg/fileutils",
"pkg/homedir",
"pkg/idtools",
"pkg/ioutils",
"pkg/jsonmessage",
"pkg/longpath",
"pkg/mount",
"pkg/pools",
"pkg/stdcopy",
"pkg/system",
"pkg/term",
"pkg/term/windows"
"pkg/namesgenerator",
"pkg/random",
]
revision = "72ba7f593fa4dbb628cf5ee83cd7daf955934cf5"
pruneopts = "UT"
revision = "092cba3727bb9b4a2f0e922cd6c0f93ea270e363"
version = "v1.13.1"
[[projects]]
digest = "1:87dcb59127512b84097086504c16595cf8fef35b9e0bfca565dfc06e198158d7"
name = "github.com/docker/go-connections"
packages = ["nat"]
pruneopts = "UT"
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
version = "v0.3.0"
[[projects]]
digest = "1:6f82cacd0af5921e99bf3f46748705239b36489464f4529a1589bc895764fb18"
name = "github.com/docker/go-units"
packages = ["."]
revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52"
version = "v0.3.2"
pruneopts = "UT"
revision = "47565b4f722fb6ceae66b95f853feed578a4a51c"
version = "v0.3.3"
[[projects]]
branch = "master"
digest = "1:f77ee3cd73cbf60434b4f72ec46a66773c555d46764399735b5f9a28505f3ece"
name = "github.com/dustin/go-coap"
packages = ["."]
pruneopts = "UT"
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
[[projects]]
name = "github.com/fsouza/go-dockerclient"
packages = ["."]
revision = "ca33ff277b527ce11b793e62f9ba244129b01caf"
version = "v1.2.0"
digest = "1:184008c955d6a3226b700c13ed0e875a7a051a759618f9bccba9b5c99f17faa5"
name = "github.com/eclipse/paho.mqtt.golang"
packages = [
".",
"packets",
]
pruneopts = "UT"
revision = "36d01c2b4cbeb3d2a12063e4880ce30800af9560"
version = "v1.1.1"
[[projects]]
digest = "1:865079840386857c809b72ce300be7580cb50d3d3129ce11bf9aa6ca2bc1934a"
name = "github.com/fatih/color"
packages = ["."]
pruneopts = "UT"
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
digest = "1:90aaa664c659918dd0055e29eb5fd6b96a17a779d423db3dd3f5764c5dbbbd4f"
name = "github.com/go-kit/kit"
packages = [
"endpoint",
@@ -134,264 +155,637 @@
"metrics",
"metrics/internal/lv",
"metrics/prometheus",
"transport/http"
"transport/grpc",
"transport/http",
]
revision = "4dc7be5d2d12881735283bcab7352178e190fc71"
version = "v0.6.0"
pruneopts = "UT"
revision = "ca4112baa34cb55091301bdc13b1420a122b1b9e"
version = "v0.7.0"
[[projects]]
digest = "1:31a18dae27a29aa074515e43a443abfd2ba6deb6d69309d8d7ce789c45f34659"
name = "github.com/go-logfmt/logfmt"
packages = ["."]
pruneopts = "UT"
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
version = "v0.3.0"
[[projects]]
digest = "1:424f6593024cdf0f6f90cba81bc69ca98df3758525e6fb248198ef15ead603a9"
name = "github.com/go-redis/redis"
packages = [
".",
"internal",
"internal/consistenthash",
"internal/hashtag",
"internal/pool",
"internal/proto",
"internal/singleflight",
"internal/util",
]
pruneopts = "UT"
revision = "7f89fbac80bcc62ce920b6dbc6ca60238d7725d1"
version = "v6.15.0"
[[projects]]
digest = "1:c4a2528ccbcabf90f9f3c464a5fc9e302d592861bbfd0b7135a7de8a943d0406"
name = "github.com/go-stack/stack"
packages = ["."]
pruneopts = "UT"
revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
version = "v1.7.0"
[[projects]]
digest = "1:8dfa0b1b7c4b7398d46340aef0f2a58aad5a75959eeeb79e023ab826f0c7f7f0"
name = "github.com/go-zoo/bone"
packages = ["."]
pruneopts = "UT"
revision = "fd0aebc74e908868b09ac140fb5a53cb363884c1"
version = "1.2"
[[projects]]
branch = "master"
digest = "1:348bbd07b4a983b2f811f81948c78142de09bb9fcf25284bee5e2ab3bdd0f6f9"
name = "github.com/gocql/gocql"
packages = [
".",
"internal/lru",
"internal/murmur",
"internal/streams",
]
pruneopts = "UT"
revision = "68ae1e384be4d7cd7df0f5b77f331759d806308e"
[[projects]]
digest = "1:bbadccf3d3317ea03c0dac0b45b673b4b397c8f91a1d2eff550a3c51c4ad770e"
name = "github.com/gogo/protobuf"
packages = ["proto"]
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
version = "v1.0.0"
pruneopts = "UT"
revision = "636bf0302bc95575d69441b25a2603156ffdddf1"
version = "v1.1.1"
[[projects]]
digest = "1:4c0989ca0bcd10799064318923b9bc2db6b4d6338dd75f3f2d86c3511aaaf5cf"
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
version = "v1.0.0"
[[projects]]
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
packages = [
"proto",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/timestamp",
]
pruneopts = "UT"
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
version = "v1.2.0"
[[projects]]
name = "github.com/jinzhu/gorm"
packages = [
".",
"dialects/postgres"
]
revision = "6ed508ec6a4ecb3531899a69cbc746ccf65a4166"
version = "v1.9.1"
[[projects]]
branch = "master"
name = "github.com/jinzhu/inflection"
digest = "1:4a0c6bb4805508a6287675fac876be2ac1182539ca8a32468d8128882e9d5009"
name = "github.com/golang/snappy"
packages = ["."]
revision = "04140366298a54a039076d798123ffa108fff46c"
pruneopts = "UT"
revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a"
[[projects]]
digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d"
name = "github.com/gorilla/websocket"
packages = ["."]
pruneopts = "UT"
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
version = "v1.4.0"
[[projects]]
digest = "1:364b908b9b27b97ab838f2f6f1b1f46281fa29b978a037d72a9b1d4f6d940190"
name = "github.com/hailocab/go-hostpool"
packages = ["."]
pruneopts = "UT"
revision = "e80d13ce29ede4452c43dea11e79b9bc8a15b478"
[[projects]]
branch = "master"
digest = "1:6727359c2ab1efe714cc87acb36be6cf15a3aae078d8ae736a17fec787dab307"
name = "github.com/hokaccha/go-prettyjson"
packages = ["."]
pruneopts = "UT"
revision = "f579f869bbfea48a61fbea09207cadd1aaeceb83"
[[projects]]
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
name = "github.com/inconshreveable/mousetrap"
packages = ["."]
pruneopts = "UT"
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0"
[[projects]]
digest = "1:65b0a07f85f7b5cc019c26775efc278a155a5dea0a8aa617c980e8308d16bc55"
name = "github.com/influxdata/influxdb"
packages = [
"client/v2",
"models",
"pkg/escape",
]
pruneopts = "UT"
revision = "a2ba6e7654fb078f8a3f5add1f8d935df38161bd"
version = "v1.6.4"
[[projects]]
branch = "master"
digest = "1:a64e323dc06b73892e5bb5d040ced475c4645d456038333883f58934abbf6f72"
name = "github.com/kr/logfmt"
packages = ["."]
pruneopts = "UT"
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
[[projects]]
branch = "master"
digest = "1:1e537dfb7b4d543e25efa2c812084dd65736f8010979c1b7e167a5b876563199"
name = "github.com/lib/pq"
packages = [
".",
"hstore",
"oid"
"oid",
]
revision = "88edab0803230a3898347e77b474f8c1820a1f20"
pruneopts = "UT"
revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
[[projects]]
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "UT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
digest = "1:d4d17353dbd05cb52a2a52b7fe1771883b682806f68db442b436294926bbfafb"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6"
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
pruneopts = "UT"
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
digest = "1:27fa1b385644151ee1fc0c8b04f1f86963d9701335491b94488fd3912e370354"
name = "github.com/mongodb/mongo-go-driver"
packages = [
"bson",
"bson/bsoncodec",
"bson/bsonrw",
"bson/bsontype",
"bson/primitive",
"event",
"internal",
"mongo",
"mongo/options",
"mongo/readconcern",
"mongo/readpref",
"mongo/writeconcern",
"tag",
"version",
"x/bsonx",
"x/bsonx/bsoncore",
"x/mongo/driver",
"x/mongo/driver/auth",
"x/mongo/driver/auth/internal/gssapi",
"x/mongo/driver/session",
"x/mongo/driver/topology",
"x/mongo/driver/uuid",
"x/network/address",
"x/network/command",
"x/network/compressor",
"x/network/connection",
"x/network/connstring",
"x/network/description",
"x/network/result",
"x/network/wiremessage",
]
pruneopts = "UT"
revision = "3cfdc9ffcf780eede57d27ec26a3edad4c9ab6a9"
version = "v0.2.0"
[[projects]]
digest = "1:4ce94f3d2074662c04677db471456feb56078244ebcfe3789e6e95901c085bc5"
name = "github.com/nats-io/go-nats"
packages = [
".",
"encoders/builtin",
"util"
"util",
]
revision = "d66cb54e6b7bdd93f0b28afc8450d84c780dfb68"
version = "v1.4.0"
pruneopts = "UT"
revision = "fb0396ee0bdb8018b0fef30d6d1de798ce99cd05"
version = "v1.6.0"
[[projects]]
digest = "1:c3cd663f2f30b92536b9f290ac85c6310dae36a14cb8961553ae9ccf0d85ae41"
name = "github.com/nats-io/nuid"
packages = ["."]
pruneopts = "UT"
revision = "289cccf02c178dc782430d534e3c1f5b72af807f"
version = "v1.0.0"
[[projects]]
digest = "1:ee4d4af67d93cc7644157882329023ce9a7bcfce956a079069a9405521c7cc8d"
name = "github.com/opencontainers/go-digest"
packages = ["."]
pruneopts = "UT"
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
version = "v1.0.0-rc1"
[[projects]]
digest = "1:11db38d694c130c800d0aefb502fb02519e514dc53d9804ce51d1ad25ec27db6"
name = "github.com/opencontainers/image-spec"
packages = [
"specs-go",
"specs-go/v1"
"specs-go/v1",
]
pruneopts = "UT"
revision = "d60099175f88c47cd379c4738d158884749ed235"
version = "v1.0.1"
[[projects]]
digest = "1:1869683e323ebff2bdf8adcb560f82bf6f8d94019d35099e3403f7df12e9c07e"
name = "github.com/opencontainers/runc"
packages = [
"libcontainer/system",
"libcontainer/user"
"libcontainer/user",
]
pruneopts = "UT"
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
version = "v0.1.1"
[[projects]]
digest = "1:3b830e06971be8448e7c9e57c9cc15ce53ab7e8846e7b7c39110a177c7fae466"
name = "github.com/ory/dockertest"
packages = [
"docker",
"docker/opts",
"docker/pkg/archive",
"docker/pkg/fileutils",
"docker/pkg/homedir",
"docker/pkg/idtools",
"docker/pkg/ioutils",
"docker/pkg/jsonmessage",
"docker/pkg/longpath",
"docker/pkg/mount",
"docker/pkg/pools",
"docker/pkg/stdcopy",
"docker/pkg/system",
"docker/pkg/term",
"docker/pkg/term/windows",
"docker/types",
"docker/types/blkiodev",
"docker/types/container",
"docker/types/filters",
"docker/types/mount",
"docker/types/network",
"docker/types/registry",
"docker/types/strslice",
"docker/types/versions",
]
pruneopts = "UT"
revision = "2e92e7784b6fb199fd168aa46269a2f1b34f299e"
version = "v3.3.0"
[[projects]]
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "UT"
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
digest = "1:26663fafdea73a38075b07e8e9d82fc0056379d2be8bb4e13899e8fda7c7dd23"
name = "github.com/prometheus/client_golang"
packages = [
"prometheus",
"prometheus/promhttp"
"prometheus/internal",
"prometheus/promhttp",
]
revision = "c5b7fccd204277076155f10851dad72b76a49317"
version = "v0.8.0"
pruneopts = "UT"
revision = "abad2d1bd44235a26707c172eab6bca5bf2dbad3"
version = "v0.9.1"
[[projects]]
branch = "master"
digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b"
name = "github.com/prometheus/client_model"
packages = ["go"]
pruneopts = "UT"
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
digest = "1:e469cd65badf7694aeb44874518606d93c1d59e7735d3754ad442782437d3cc3"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model"
"model",
]
revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0"
pruneopts = "UT"
revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c"
[[projects]]
branch = "master"
digest = "1:570784e0ddbf67f14087c6d8cfb7b999b9c55e75518a755e88cb202566ea8a17"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs"
"xfs",
]
revision = "54d17b57dd7d4a3aa092476596b3f8a933bde349"
pruneopts = "UT"
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
[[projects]]
branch = "master"
digest = "1:89cf776c0a621a34017c819b66b9e0f0bb076d3c5b9ca74763fb89570e140038"
name = "github.com/rubenv/sql-migrate"
packages = [
".",
"sqlparse",
]
pruneopts = "UT"
revision = "ba2c6a7295c59448dbc195cef2f41df5163b3892"
[[projects]]
digest = "1:274f67cb6fed9588ea2521ecdac05a6d62a8c51c074c1fccc6a49a40ba80e925"
name = "github.com/satori/go.uuid"
packages = ["."]
pruneopts = "UT"
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
version = "v1.2.0"
[[projects]]
digest = "1:9e9193aa51197513b3abcb108970d831fbcf40ef96aa845c4f03276e1fa316d2"
name = "github.com/sirupsen/logrus"
packages = ["."]
pruneopts = "UT"
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
[[projects]]
digest = "1:d3ffbc45dfe6929a4ba313464c2d761d96325d306eda29512da56c034db2a4dd"
name = "github.com/sony/gobreaker"
packages = ["."]
revision = "e9556a45379ef1da12e54847edb2fb3d7d566f36"
version = "0.3.0"
pruneopts = "UT"
revision = "b2a34562d02c4d5fd6645399c69e00141fb24e5a"
version = "0.4.0"
[[projects]]
digest = "1:645cabccbb4fa8aab25a956cbcbdf6a6845ca736b2c64e197ca7cbb9d210b939"
name = "github.com/spf13/cobra"
packages = ["."]
pruneopts = "UT"
revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385"
version = "v0.0.3"
[[projects]]
digest = "1:9424f440bba8f7508b69414634aef3b2b3a877e522d8a4624692412805407bb7"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = "UT"
revision = "583c0c0531f06d5278b7d917446061adc344b5cd"
version = "v1.0.1"
[[projects]]
digest = "1:c40d65817cdd41fac9aa7af8bed56927bb2d6d47e4fea566a74880f5c2b1c41e"
name = "github.com/stretchr/testify"
packages = ["assert"]
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
packages = [
"assert",
"require",
]
pruneopts = "UT"
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[[projects]]
digest = "1:03aa6e485e528acb119fb32901cf99582c380225fc7d5a02758e08b180cb56c3"
name = "github.com/ugorji/go"
packages = ["codec"]
revision = "9831f2c3ac1068a78f50999a30db84270f647af6"
version = "v1.1"
[[projects]]
name = "go.uber.org/atomic"
packages = ["."]
revision = "8474b86a5a6f79c443ce4b2992817ff32cf208b8"
version = "v1.3.1"
[[projects]]
name = "go.uber.org/multierr"
packages = ["."]
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
version = "v1.1.0"
[[projects]]
name = "go.uber.org/zap"
packages = [
".",
"buffer",
"internal/bufferpool",
"internal/color",
"internal/exit",
"zapcore"
]
revision = "35aad584952c3e7020db7b839f6b102de6271f89"
version = "v1.7.1"
pruneopts = "UT"
revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab"
version = "v1.1.1"
[[projects]]
branch = "master"
digest = "1:40fdfd6ab85ca32b6935853bbba35935dcb1d796c8135efd85947566c76e662e"
name = "github.com/xdg/scram"
packages = ["."]
pruneopts = "UT"
revision = "7eeb5667e42c09cb51bf7b7c28aea8c56767da90"
[[projects]]
branch = "master"
digest = "1:f5c1d04bc09c644c592b45b9f0bad4030521b1a7d11c7dadbb272d9439fa6e8e"
name = "github.com/xdg/stringprep"
packages = ["."]
pruneopts = "UT"
revision = "73f8eece6fdcd902c185bf651de50f3828bed5ed"
[[projects]]
branch = "master"
digest = "1:7310f5459b88177d24e26c5ec7deb100b78ec03c0437d7a7aaec3c7eac333a55"
name = "golang.org/x/crypto"
packages = [
"bcrypt",
"blowfish",
"ssh/terminal"
"pbkdf2",
"ssh/terminal",
]
revision = "374053ea96cb300f8671b8d3b07edeeb06e203b4"
pruneopts = "UT"
revision = "4d3f4d9ffa16a13f451c3b2999e9c49e9750bf06"
[[projects]]
branch = "master"
digest = "1:70e155783b8dfd4f7697243384e2f361930ff0751459580769ce44d511f38b05"
name = "golang.org/x/net"
packages = [
"context",
"context/ctxhttp"
"http/httpguts",
"http2",
"http2/hpack",
"idna",
"internal/socks",
"internal/timeseries",
"proxy",
"trace",
"websocket",
]
revision = "24dd3780ca4f75fed9f321890729414a4b5d3f13"
pruneopts = "UT"
revision = "f73e4c9ed3b7ebdd5f699a16a880c2b1994e50dd"
[[projects]]
branch = "master"
digest = "1:e0140c0c868c6e0f01c0380865194592c011fe521d6e12d78bfd33e756fe018a"
name = "golang.org/x/sync"
packages = ["semaphore"]
pruneopts = "UT"
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
[[projects]]
branch = "master"
digest = "1:66e34aa65b83061616603ba83517a4523c7261e18afd01c769dd215614bf7d70"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
"windows",
]
revision = "01acb38716e021ed1fc03a602bdb5838e1358c5e"
pruneopts = "UT"
revision = "7dfd1290c7917b7ba22824b9d24954ab3002fe24"
[[projects]]
digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18"
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable",
]
pruneopts = "UT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
branch = "master"
digest = "1:cd018653a358d4b743a9d3bee89e825521f2ab2f2ec0770164bf7632d8d73ab7"
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
pruneopts = "UT"
revision = "86e600f69ee4704c6efbf6a2a40a5c10700e76c2"
[[projects]]
digest = "1:c3ad9841823db6da420a5625b367913b4ff54bbe60e8e3c98bd20e243e62e2d2"
name = "google.golang.org/grpc"
packages = [
".",
"balancer",
"balancer/base",
"balancer/roundrobin",
"codes",
"connectivity",
"credentials",
"encoding",
"encoding/proto",
"grpclog",
"internal",
"internal/backoff",
"internal/channelz",
"internal/envconfig",
"internal/grpcrand",
"internal/transport",
"keepalive",
"metadata",
"naming",
"peer",
"resolver",
"resolver/dns",
"resolver/passthrough",
"stats",
"status",
"tap",
]
pruneopts = "UT"
revision = "2e463a05d100327ca47ac218281906921038fd95"
version = "v1.16.0"
[[projects]]
digest = "1:fa9a7c0ef59217bd22f32eb7cc027894d73f340a5633258cc079dec025db6a7f"
name = "gopkg.in/gorp.v1"
packages = ["."]
pruneopts = "UT"
revision = "c87af80f3cc5036b55b83d77171e156791085e2e"
version = "v1.7.1"
[[projects]]
digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a"
name = "gopkg.in/inf.v0"
packages = ["."]
pruneopts = "UT"
revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf"
version = "v0.9.1"
[[projects]]
digest = "1:de4bc452b049bde272d99e944aabcf218dc22df0a478a42a7107932f82430ae2"
name = "gopkg.in/ory-am/dockertest.v3"
packages = ["."]
revision = "15c8e8835bba04e0d7c2b57958ffe294d5e643dc"
version = "v3.1.6"
pruneopts = "UT"
revision = "9f1141b78507d23603f6be530587664ebb63a8d2"
version = "v3.3.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "c1b28e90e21e838cec2d16c95d8eb414b5de9bbe9667acb01b5375db047cc6db"
input-imports = [
"github.com/asaskevich/govalidator",
"github.com/cisco/senml",
"github.com/dereulenspiegel/coap-mux",
"github.com/dgrijalva/jwt-go",
"github.com/docker/docker/pkg/namesgenerator",
"github.com/dustin/go-coap",
"github.com/eclipse/paho.mqtt.golang",
"github.com/fatih/color",
"github.com/go-kit/kit/endpoint",
"github.com/go-kit/kit/log",
"github.com/go-kit/kit/metrics",
"github.com/go-kit/kit/metrics/prometheus",
"github.com/go-kit/kit/transport/grpc",
"github.com/go-kit/kit/transport/http",
"github.com/go-redis/redis",
"github.com/go-zoo/bone",
"github.com/gocql/gocql",
"github.com/gogo/protobuf/proto",
"github.com/golang/protobuf/proto",
"github.com/gorilla/websocket",
"github.com/hokaccha/go-prettyjson",
"github.com/influxdata/influxdb/client/v2",
"github.com/lib/pq",
"github.com/mongodb/mongo-go-driver/mongo",
"github.com/nats-io/go-nats",
"github.com/prometheus/client_golang/prometheus",
"github.com/prometheus/client_golang/prometheus/promhttp",
"github.com/rubenv/sql-migrate",
"github.com/satori/go.uuid",
"github.com/sony/gobreaker",
"github.com/spf13/cobra",
"github.com/stretchr/testify/assert",
"github.com/stretchr/testify/require",
"golang.org/x/crypto/bcrypt",
"golang.org/x/net/context",
"google.golang.org/grpc",
"google.golang.org/grpc/codes",
"google.golang.org/grpc/credentials",
"google.golang.org/grpc/status",
"gopkg.in/ory-am/dockertest.v3",
]
solver-name = "gps-cdcl"
solver-version = 1
+59 -23
View File
@@ -3,24 +3,24 @@
version = "9.0.0"
[[constraint]]
branch = "master"
name = "github.com/cisco/senml"
[[constraint]]
branch = "master"
name = "github.com/dereulenspiegel/coap-mux"
[[constraint]]
name = "github.com/dgrijalva/jwt-go"
version = "3.2.0"
[[constraint]]
branch = "master"
name = "github.com/dustin/go-coap"
name = "github.com/eclipse/paho.mqtt.golang"
version = "1.1.1"
[[constraint]]
name = "github.com/fatih/color"
version = "1.7.0"
[[constraint]]
name = "github.com/go-kit/kit"
version = "0.6.0"
version = "0.7.0"
[[constraint]]
name = "github.com/go-zoo/bone"
@@ -28,23 +28,23 @@
[[constraint]]
name = "github.com/golang/protobuf"
version = "1.0.0"
[[constraint]]
name = "github.com/jinzhu/gorm"
version = "1.9.1"
version = "1.2.0"
[[constraint]]
name = "github.com/hokaccha/go-prettyjson"
branch = "master"
[[constraint]]
name = "github.com/lib/pq"
branch = "master"
[[constraint]]
name = "github.com/nats-io/go-nats"
version = "1.4.0"
version = "1.6.0"
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.8.0"
version = "0.9.1"
[[constraint]]
name = "github.com/satori/go.uuid"
@@ -52,27 +52,63 @@
[[constraint]]
name = "github.com/sony/gobreaker"
version = "0.3.0"
version = "0.4.0"
[[constraint]]
name = "github.com/spf13/cobra"
version = "0.0.3"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
version = "1.2.2"
[[constraint]]
name = "go.uber.org/zap"
version = "1.7.1"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
branch = "master"
[[constraint]]
name = "gopkg.in/ory-am/dockertest.v3"
version = "3.1.6"
version = "3.3.2"
[[constraint]]
name = "github.com/gorilla/websocket"
version = "1.2.0"
version = "1.4.0"
[[constraint]]
name = "github.com/rubenv/sql-migrate"
branch = "master"
[[constraint]]
name = "github.com/influxdata/influxdb"
version = "1.6.4"
[[constraint]]
name = "github.com/mongodb/mongo-go-driver"
version = "0.0.5"
[[constraint]]
name = "github.com/gocql/gocql"
branch = "master"
[[constraint]]
name = "github.com/dereulenspiegel/coap-mux"
branch = "master"
[[constraint]]
name = "github.com/dustin/go-coap"
branch = "master"
[[constraint]]
name = "github.com/gogo/protobuf"
version = "1.1.1"
[[constraint]]
name = "google.golang.org/grpc"
version = "1.16.0"
[[constraint]]
name = "github.com/go-redis/redis"
version = "v6.15.0"
[prune]
go-tests = true
+1 -1
View File
@@ -176,7 +176,7 @@
END OF TERMS AND CONDITIONS
Copyright 2015-2018 Mainflux
Copyright 2015-2019 Mainflux
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+14 -13
View File
@@ -7,7 +7,7 @@
[[drasko]]
Name = "Drasko DRASKOVIC"
Email = "drasko.draskovic@mainflux.com"
Email = "drasko@mainflux.com"
GitHub = "drasko"
# However, this role serves only in dead-lock events, or in a special and very rare cases
@@ -24,22 +24,23 @@
[maintainers]
[[aleksandar]]
Name = "Aleksandar NOVAKOVIC"
Email = "aleksandar@mainflux.com"
GitHub = "anovakovic01"
[[dusan]]
Name = "Dusan BOROVCANIN"
Email = "dusan@mainflux.com"
GitHub = "dusanb94"
[[manuel]]
Name = "Manuel IMPERIALE"
Email = "manuel.imperiale@gmail.com"
Email = "manuel@mainflux.com"
GitHub = "manuIO"
[[janko]]
Name = "Janko ISIDOROVIC"
Email = "janko.isidorovic@mainflux.com"
GitHub = "janko-isidorovic"
[[nikola]]
Name = "Nikola MARCETIC"
Email = "nikola.marcetic@mainflux.com"
Email = "nikola@mainflux.com"
GitHub = "nmarcetic"
[[dejan]]
Name = "Dejan MIJIC"
Email = "dejan.mijic@mainflux.com"
GitHub = "mijicd"
+86 -12
View File
@@ -1,6 +1,12 @@
## Copyright (c) 2015-2019
## Mainflux
##
## SPDX-License-Identifier: Apache-2.0
BUILD_DIR = build
SERVICES = manager http normalizer ws
SERVICES = users things http normalizer ws coap lora influxdb-writer influxdb-reader mongodb-writer mongodb-reader cassandra-writer cassandra-reader cli bootstrap
DOCKERS = $(addprefix docker_,$(SERVICES))
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
CGO_ENABLED ?= 0
GOOS ?= linux
@@ -9,34 +15,86 @@ define compile_service
endef
define make_docker
docker build --build-arg SVC_NAME=$(subst docker_,,$(1)) --tag=mainflux/$(subst docker_,,$(1)) -f docker/Dockerfile .
docker build --no-cache --build-arg SVC_NAME=$(subst docker_,,$(1)) --tag=mainflux/$(subst docker_,,$(1)) -f docker/Dockerfile .
endef
all: $(SERVICES)
define make_docker_dev
docker build --build-arg SVC_NAME=$(subst docker_dev_,,$(1)) --tag=mainflux/$(subst docker_dev_,,$(1)) -f docker/Dockerfile.dev ./build
endef
.PHONY: all $(SERVICES) dockers latest release
all: $(SERVICES) mqtt
.PHONY: all $(SERVICES) dockers dockers_dev latest release mqtt ui
clean:
rm -rf ${BUILD_DIR}
rm -rf mqtt/node_modules
cleandocker: cleanghost
# Stop all containers (if running)
docker-compose -f docker/docker-compose.yml stop
# Remove mainflux containers
docker ps -f name=mainflux -aq | xargs -r docker rm
# Remove old mainflux images
docker images -q mainflux\/* | xargs -r docker rmi
# Clean ghost docker images
cleanghost:
# Remove exited containers
docker ps -f status=dead -f status=exited -aq | xargs -r docker rm -v
# Remove unused images
docker images -f dangling=true -q | xargs -r docker rmi
# Remove unused volumes
docker volume ls -f dangling=true -q | xargs -r docker volume rm
install:
cp ${BUILD_DIR}/* $(GOBIN)
proto:
protoc --go_out=. *.proto
test:
go test -v -race -count 1 -tags test $(shell go list ./... | grep -v 'vendor\|cmd')
$(SERVICES): proto
proto:
protoc --gofast_out=plugins=grpc:. *.proto
$(SERVICES):
$(call compile_service,$(@))
$(DOCKERS):
$(call make_docker,$(@))
dockers: $(DOCKERS)
docker_ui:
$(MAKE) -C ui docker
docker_mqtt:
# MQTT Docker build must be done from root dir because it copies .proto files
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile .
dockers: $(DOCKERS) docker_ui docker_mqtt
$(DOCKERS_DEV):
$(call make_docker_dev,$(@))
dockers_dev: $(DOCKERS_DEV)
ui:
$(MAKE) -C ui
mqtt:
cd mqtt && npm install
define docker_push
for svc in $(SERVICES); do \
docker push mainflux/$$svc:$(1); \
done
docker push mainflux/ui:$(1)
docker push mainflux/mqtt:$(1)
endef
changelog:
git log $(shell git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s"
latest: dockers
for svc in $(SERVICES); do \
docker push mainflux/$$svc; \
done
$(call docker_push,latest)
release:
$(eval version = $(shell git describe --abbrev=0 --tags))
@@ -44,5 +102,21 @@ release:
$(MAKE) dockers
for svc in $(SERVICES); do \
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
docker push mainflux/$$svc:$(version); \
done
docker tag mainflux/ui mainflux/ui:$(version)
docker tag mainflux/mqtt mainflux/mqtt:$(version)
$(call docker_push,$(version))
rundev:
cd scripts && ./run.sh
run:
docker-compose -f docker/docker-compose.yml up
runui:
$(MAKE) -C ui run
runlora:
docker-compose -f docker/docker-compose.yml up -d
docker-compose -f docker/addons/influxdb-writer/docker-compose.yml up -d
docker-compose -f docker/addons/lora-adapter/docker-compose.yml up
+83 -8
View File
@@ -16,16 +16,25 @@ for building complex IoT solutions.
For more details, check out the [official documentation][docs].
## Features
Mainflux is member of the [Linux Foundation][lf] and an active contributor
to the [EdgeX Foundry][edgex] project. It has been made with :heart: by [Mainflux company][company],
which maintains the project and offers professional services around it.
- Protocol bridging (i.e. HTTP, MQTT, WebSocket, CoAP)
## Features
- Multi-protocol connectivity and protocol bridging (HTTP, MQTT, WebSocket and CoAP)
- Device management and provisioning
- Fine-grained access control
- Storage support (Cassandra, InfluxDB and MongoDB)
- Platform logging and instrumentation support
- Container-based deployment using [Docker][docker]
## Quickstart
- Event sourcing
- Container-based deployment using [Docker][docker] and [Kubernetes][kubernetes]
- [LoRaWAN][lora] network integration
- SDK
- CLI
- Small memory footprint and fast execution
- Domain-driven design architecture, high-quality code and test coverage
## Install
Before proceeding, install the following prerequisites:
- [Docker](https://docs.docker.com/install/)
@@ -37,20 +46,65 @@ Once everything is installed, execute the following commands from project root:
docker-compose -f docker/docker-compose.yml up -d
```
## Contributing
This will bring up all Mainflux dockers and inter-connect them in the composition.
## Usage
Best way to quickstart using Mainflux is via CLI:
```
make cli
./build/mainflux-cli version
```
> Mainflux CLI can also be downloaded as a tarball from [offical release page][rel]
If this works, head to [official documentation][docs] to understand Mainflux provisioning and messaging.
## Documentation
Official documentation is hosted at [Mainflux Read The Docs page][docs].
Documentation is auto-generated from Markdown files in `./docs` directory.
If you spot an error or need for corrections, please let us know - or even better: send us a PR.
Additional practical information, news and tutorials can be found on the [Mainflux blog][blog].
## Authors
Main architect and BDFL of Mainflux project is [@drasko][drasko].
Additionally, [@nmarcetic][nikola] and [@janko-isidorovic][janko] assured
overall architecture and design, while [@manuio][manu] and [@darkodraskovic][darko]
helped with crafting initial implementation and continiusly work on the project evolutions.
Besides them, Mainflux is constantly improved and actively
developed by [@anovakovic01][alex], [@dusanb94][dusan], [@srados][sava],
[@gsaleh][george], [@blokovi][iva], [@chombium][kole] and a large set of contributors.
Maintainers are listed in [MAINTAINERS](MAINTAINERS) file.
Mainflux team would like to give special thanks to [@mijicd][dejan] for his monumental work
on designing and implementing highly improved and optimized version of the platform,
and [@malidukica][dusanm] for his effort on implementing initial user interface.
## Contributing
Thank you for your interest in Mainflux and wish to contribute!
1. Take a look at our [open issues](https://github.com/mainflux/mainflux/issues).
2. Checkout the [contribution guide](.github/CONTRIBUTING.md) to learn more about our style and conventions.
2. Checkout the [contribution guide](CONTRIBUTING.md) to learn more about our style and conventions.
3. Make your changes compatible to our workflow.
## Community
### We're Hiring
If you are interested in working professionally on Mainflux,
please head to company's [careers page][careers] or shoot us an e-mail at <careers@mainflux.com>.
Note that the best way to grab our attention is by sending PRs :sunglasses:.
## Community
- [Google group][forum]
- [Gitter][gitter]
- [Twitter][twitter]
## License
[Apache-2.0](LICENSE)
[banner]: https://github.com/mainflux/mainflux/blob/master/docs/img/gopherBanner.jpg
[ci-badge]: https://semaphoreci.com/api/v1/mainflux/mainflux/branches/master/badge.svg
[ci-url]: https://semaphoreci.com/mainflux/mainflux
@@ -65,3 +119,24 @@ Thank you for your interest in Mainflux and wish to contribute!
[cov-url]: https://codecov.io/gh/mainflux/mainflux
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
[twitter]: https://twitter.com/mainflux
[lora]: https://lora-alliance.org/
[kubernetes]: https://kubernetes.io/
[rel]: https://github.com/mainflux/mainflux/releases
[careers]: https://www.mainflux.com/careers.html
[lf]: https://www.linuxfoundation.org/
[edgex]: https://www.edgexfoundry.org/
[company]: https://www.mainflux.com/
[blog]: https://medium.com/mainflux-iot-platform
[drasko]: https://github.com/drasko
[nikola]: https://github.com/nmarcetic
[dejan]: https://github.com/mijicd
[manu]: https://github.com/manuIO
[darko]: https://github.com/darkodraskovic
[janko]: https://github.com/janko-isidorovic
[alex]: https://github.com/anovakovic01
[dusan]: https://github.com/dusanb94
[sava]: https://github.com/srados
[george]: https://github.com/gesaleh
[iva]: https://github.com/blokovi
[kole]: https://github.com/chombium
[dusanm]: https://github.com/malidukica
+20
View File
@@ -0,0 +1,20 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package mainflux
// Response contains HTTP response specific methods.
type Response interface {
// Code returns HTTP response code.
Code() int
// Headers returns map of HTTP headers with their values.
Headers() map[string]string
// Empty indicates if HTTP response has content.
Empty() bool
}
+125
View File
@@ -0,0 +1,125 @@
# BOOTSTRAP SERVICE
New devices need to be configured properly and connected to the Mainflux. Bootstrap service is used in order to accomplish that. This service provides the following features:
1) Creating new Mainflux Things
2) Providing basic configuration for the newly created Things
3) Enabling/disabling Things
Pre-provisioning a new Thing is as simple as sending Configuration data to the Bootstrap service. Once the Thing is online, it sends a request for initial config to Bootstrap service. Bootstrap service provides an API for enabling and disabling Things. Only enabled Things can exchange messages over Mainflux. Bootstrapping does not implicitly enable Things, it has to be done manually.
In order to bootstrap successfully, the Thing needs to send bootstrapping request to the specific URL, as well as a secret key. This key and URL are pre-provisioned during the manufacturing process. If the Thing is provisioned on the Bootstrap service side, the corresponding configuration will be sent as a response. Otherwise, the Thing will be saved so that it can be provisioned later.
***Thing Configuration***
Thing Configuration consists of two logical parts: the custom configuration that can be interpreted by the Thing itself and Mainflux-related configuration. Mainflux config contains:
1) corresponding Mainflux Thing ID
2) corresponding Mainflux Thing key
3) list of the Mainflux channels the Thing is connected to
>Note: list of channels contains IDs of the Mainflux channels. These channels are _pre-provisioned_ on the Mainflux side and, unlike corresponding Mainflux Thing, Bootstrap service is not able to create Mainflux Channels.
Enabling and disabling Thing (adding Thing to/from whitelist) is as simple as connecting corresponding Mainflux Thing to the given list of Channels. Configuration keeps _state_ of the Thing:
| State | What it means |
|----------|--------------------------------------------------------|
| Inactive | Thing is created, but isn't enabled |
| Active | Thing is able to communicate using Mainflux |
Switching between states `Active` and `Inactive` enables and disables Thing, respectively.
## Configuration
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
| Variable | Description | Default |
|-------------------------------|-------------------------------------------------------------------------|-----------------------|
| MF_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | error |
| MF_BOOTSTRAP_DB_HOST | Database host address | localhost |
| MF_BOOTSTRAP_DB_PORT | Database host port | 5432 |
| MF_BOOTSTRAP_DB_USER | Database user | mainflux |
| MF_BOOTSTRAP_DB_PASS | Database password | mainflux |
| MF_BOOTSTRAP_DB | Name of the database used by the service | bootstrap |
| MF_BOOTSTRAP_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
| MF_BOOTSTRAP_DB_SSL_CERT | Path to the PEM encoded certificate file | |
| MF_BOOTSTRAP_DB_SSL_KEY | Path to the PEM encoded key file | |
| MF_BOOTSTRAP_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
| MF_BOOTSTRAP_CLIENT_TLS | Flag that indicates if TLS should be turned on | false |
| MF_BOOTSTRAP_CA_CERTS | Path to trusted CAs in PEM format | |
| MF_BOOTSTRAP_PORT | Bootstrap service HTTP port | 8180 |
| MF_BOOTSTRAP_SERVER_CERT | Path to server certificate in pem format | |
| MF_BOOTSTRAP_SERVER_KEY | Path to server key in pem format | |
| MF_SDK_BASE_URL | Base url for Mainflux SDK | http://localhost |
| MF_SDK_THINGS_PREFIX | SDK prefix for Things service | |
| MF_USERS_URL | Users service URL | localhost:8181 |
| MF_THINGS_ES_URL | Things service event source URL | localhost:6379 |
| MF_THINGS_ES_PASS | Things service event source password | |
| MF_THINGS_ES_DB | Things service event source database | 0 |
| MF_BOOTSTRAP_INSTANCE_NAME | Bootstrap service instance name | bootstrap |
## Deployment
The service itself is distributed as Docker container. The following snippet
provides a compose file template that can be used to deploy the service container
locally:
```yaml
version: "2"
bootstrap:
image: mainflux/bootstrap:latest
container_name: mainflux-bootstrap
depends_on:
- bootstrap-db
restart: on-failure
ports:
- 8200:8200
environment:
MF_BOOTSTRAP_LOG_LEVEL: [Bootstrap log level]
MF_BOOTSTRAP_DB_HOST: [Database host address]
MF_BOOTSTRAP_DB_PORT: [Database host port]
MF_BOOTSTRAP_DB_USER: [Database user]
MF_BOOTSTRAP_DB_PASS: [Database password]
MF_BOOTSTRAP_DB: [Name of the database used by the service]
MF_BOOTSTRAP_DB_SSL_MODE: [SSL mode to connect to the database with]
MF_BOOTSTRAP_DB_SSL_CERT: [Path to the PEM encoded certificate file]
MF_BOOTSTRAP_DB_SSL_KEY: [Path to the PEM encoded key file]
MF_BOOTSTRAP_DB_SSL_ROOT_CERT: [Path to the PEM encoded root certificate file]
MF_BOOTSTRAP_CLIENT_TLS: [Boolean value to enable/disable client TLS]
MF_BOOTSTRAP_CA_CERTS: [Path to trusted CAs in PEM format]
MF_BOOTSTRAP_PORT: 8200
MF_BOOTSTRAP_SERVER_CERT: [String path to server cert in pem format]
MF_BOOTSTRAP_SERVER_KEY: [String path to server key in pem format]
MF_SDK_BASE_URL: [Base SDK URL for the Mainflux services]
MF_SDK_THINGS_PREFIX: [SDK prefix for Things service]
MF_USERS_URL: [Users service URL]
MF_THINGS_ES_URL: [Things service event source URL]
MF_THINGS_ES_PASS: [Things service event source password]
MF_THINGS_ES_DB: [Things service event source database]
MF_BOOTSTRAP_INSTANCE_NAME: [Bootstrap service instance name]
```
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux
# compile the service
make bootstrap
# copy binary to bin
make install
# set the environment variables and run the service
MF_BOOTSTRAP_LOG_LEVEL=[Bootstrap log level] MF_BOOTSTRAP_DB_HOST=[Database host address] MF_BOOTSTRAP_DB_PORT=[Database host port] MF_BOOTSTRAP_DB_USER=[Database user] MF_BOOTSTRAP_DB_PASS=[Database password] MF_BOOTSTRAP_DB=[Name of the database used by the service] MF_BOOTSTRAP_DB_SSL_MODE=[SSL mode to connect to the database with] MF_BOOTSTRAP_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_BOOTSTRAP_DB_SSL_KEY=[Path to the PEM encoded key file] MF_BOOTSTRAP_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_BOOTSTRAP_CLIENT_TLS=[Boolean value to enable/disable client TLS] MF_BOOTSTRAP_CA_CERTS=[Path to trusted CAs in PEM format] MF_BOOTSTRAP_PORT=[Service HTTP port] MF_BOOTSTRAP_SERVER_CERT=[Path to server certificate] MF_BOOTSTRAP_SERVER_KEY=[Path to server key] MF_SDK_BASE_URL=[Base SDK URL for the Mainflux services] MF_SDK_THINGS_PREFIX=[SDK prefix for Things service] MF_USERS_URL=[Users service URL] $GOBIN/mainflux-bootstrap
```
Setting `MF_BOOTSTRAP_CA_CERTS` expects a file in PEM format of trusted CAs. This will enable TLS against the Users gRPC endpoint trusting only those CAs that are provided.
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yml).
[doc]: http://mainflux.readthedocs.io
+9
View File
@@ -0,0 +1,9 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
// Package api contains implementation of things service HTTP API.
package api
+241
View File
@@ -0,0 +1,241 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package api
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/bootstrap"
)
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(addReq)
if err := req.validate(); err != nil {
return nil, err
}
channels := []bootstrap.Channel{}
for _, c := range req.Channels {
channels = append(channels, bootstrap.Channel{ID: c})
}
config := bootstrap.Config{
MFThing: req.ThingID,
ExternalID: req.ExternalID,
ExternalKey: req.ExternalKey,
MFChannels: channels,
Name: req.Name,
Content: req.Content,
}
saved, err := svc.Add(req.key, config)
if err != nil {
return nil, err
}
res := configRes{
id: saved.MFThing,
created: true,
}
return res, nil
}
}
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(entityReq)
if err := req.validate(); err != nil {
return nil, err
}
config, err := svc.View(req.key, req.id)
if err != nil {
return nil, err
}
var channels []channelRes
for _, ch := range config.MFChannels {
channels = append(channels, channelRes{
ID: ch.ID,
Name: ch.Name,
Metadata: ch.Metadata,
})
}
res := viewRes{
MFThing: config.MFThing,
MFKey: config.MFKey,
Channels: channels,
ExternalID: config.ExternalID,
ExternalKey: config.ExternalKey,
Name: config.Name,
Content: config.Content,
State: config.State,
}
return res, nil
}
}
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateReq)
if err := req.validate(); err != nil {
return nil, err
}
config := bootstrap.Config{
MFThing: req.id,
Name: req.Name,
Content: req.Content,
}
if err := svc.Update(req.key, config); err != nil {
return nil, err
}
res := configRes{
id: config.MFThing,
created: false,
}
return res, nil
}
}
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateConnReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.UpdateConnections(req.key, req.id, req.Channels); err != nil {
return nil, err
}
res := configRes{
id: req.id,
created: false,
}
return res, nil
}
}
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(listReq)
if err := req.validate(); err != nil {
return nil, err
}
page, err := svc.List(req.key, req.filter, req.offset, req.limit)
if err != nil {
return nil, err
}
switch {
case req.filter.Unknown:
res := listUnknownRes{}
for _, cfg := range page.Configs {
res.Configs = append(res.Configs, unknownRes{
ExternalID: cfg.ExternalID,
ExternalKey: cfg.ExternalKey,
})
}
return res, nil
default:
res := listRes{
Total: page.Total,
Offset: page.Offset,
Limit: page.Limit,
Configs: []viewRes{},
}
for _, cfg := range page.Configs {
var channels []channelRes
for _, ch := range cfg.MFChannels {
channels = append(channels, channelRes{
ID: ch.ID,
Name: ch.Name,
Metadata: ch.Metadata,
})
}
view := viewRes{
MFThing: cfg.MFThing,
MFKey: cfg.MFKey,
Channels: channels,
ExternalID: cfg.ExternalID,
ExternalKey: cfg.ExternalKey,
Name: cfg.Name,
Content: cfg.Content,
State: cfg.State,
}
res.Configs = append(res.Configs, view)
}
return res, nil
}
}
}
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(entityReq)
if err := req.validate(); err != nil {
return removeRes{}, err
}
if err := svc.Remove(req.key, req.id); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(bootstrapReq)
if err := req.validate(); err != nil {
return nil, err
}
cfg, err := svc.Bootstrap(req.key, req.id)
if err != nil {
return nil, err
}
return reader.ReadConfig(cfg)
}
}
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(changeStateReq)
if err := req.validate(); err != nil {
return nil, err
}
if err := svc.ChangeState(req.key, req.id, req.State); err != nil {
return nil, err
}
return stateRes{}, nil
}
}
File diff suppressed because it is too large Load Diff
+186
View File
@@ -0,0 +1,186 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
// +build !test
package api
import (
"fmt"
"time"
"github.com/mainflux/mainflux/bootstrap"
log "github.com/mainflux/mainflux/logger"
)
var _ bootstrap.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc bootstrap.Service
}
// NewLoggingMiddleware adds logging facilities to the core service.
func NewLoggingMiddleware(svc bootstrap.Service, logger log.Logger) bootstrap.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Add(key string, thing bootstrap.Config) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method add for key %s and thing %s took %s to complete", key, saved.MFThing, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Add(key, thing)
}
func (lm *loggingMiddleware) View(key, id string) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method view for key %s and thing %s took %s to complete", key, saved.MFThing, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.View(key, id)
}
func (lm *loggingMiddleware) Update(key string, thing bootstrap.Config) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method update for key %s and thing %s took %s to complete", key, thing.MFThing, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Update(key, thing)
}
func (lm *loggingMiddleware) UpdateConnections(key, id string, connections []string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method update_connections for key %s and thing %s took %s to complete", key, id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.UpdateConnections(key, id, connections)
}
func (lm *loggingMiddleware) List(key string, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method list for key %s and offset %d and limit %d took %s to complete", key, offset, limit, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.List(key, filter, offset, limit)
}
func (lm *loggingMiddleware) Remove(key, id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method remove for key %s and thing %s took %s to complete", key, id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Remove(key, id)
}
func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string) (cfg bootstrap.Config, err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method bootstrap for thing with external id %s took %s to complete", externalID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Bootstrap(externalKey, externalID)
}
func (lm *loggingMiddleware) ChangeState(key, id string, state bootstrap.State) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method change_state for key %s and thing %s took %s to complete", key, id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.ChangeState(key, id, state)
}
func (lm *loggingMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method update_channel_handler for channel %s took %s to complete", channel.ID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.UpdateChannelHandler(channel)
}
func (lm *loggingMiddleware) RemoveConfigHandler(id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method remove_config_handler for config %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.RemoveConfigHandler(id)
}
func (lm *loggingMiddleware) RemoveChannelHandler(id string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method remove_channel_handler for channel %s took %s to complete", id, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.RemoveChannelHandler(id)
}
func (lm *loggingMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method disconnect_thing_handler for channel %s and thing %s took %s to complete", channelID, thingID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.DisconnectThingHandler(channelID, thingID)
}
+143
View File
@@ -0,0 +1,143 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
// +build !test
package api
import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux/bootstrap"
)
var _ bootstrap.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc bootstrap.Service
}
// MetricsMiddleware instruments core service by tracking request count and
// latency.
func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency metrics.Histogram) bootstrap.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (mm *metricsMiddleware) Add(key string, thing bootstrap.Config) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "add").Add(1)
mm.latency.With("method", "add").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Add(key, thing)
}
func (mm *metricsMiddleware) View(id, key string) (saved bootstrap.Config, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "view").Add(1)
mm.latency.With("method", "view").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.View(id, key)
}
func (mm *metricsMiddleware) Update(key string, thing bootstrap.Config) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update").Add(1)
mm.latency.With("method", "update").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Update(key, thing)
}
func (mm *metricsMiddleware) UpdateConnections(key, id string, connections []string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_connections").Add(1)
mm.latency.With("method", "update_connections").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.UpdateConnections(key, id, connections)
}
func (mm *metricsMiddleware) List(key string, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "list").Add(1)
mm.latency.With("method", "list").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.List(key, filter, offset, limit)
}
func (mm *metricsMiddleware) Remove(id, key string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "remove").Add(1)
mm.latency.With("method", "remove").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Remove(id, key)
}
func (mm *metricsMiddleware) Bootstrap(externalKey, externalID string) (cfg bootstrap.Config, err error) {
defer func(begin time.Time) {
mm.counter.With("method", "bootstrap").Add(1)
mm.latency.With("method", "bootstrap").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Bootstrap(externalKey, externalID)
}
func (mm *metricsMiddleware) ChangeState(id, key string, state bootstrap.State) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "change_state").Add(1)
mm.latency.With("method", "change_state").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.ChangeState(id, key, state)
}
func (mm *metricsMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "update_channel").Add(1)
mm.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.UpdateChannelHandler(channel)
}
func (mm *metricsMiddleware) RemoveConfigHandler(id string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "remove_config").Add(1)
mm.latency.With("method", "remove_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.RemoveConfigHandler(id)
}
func (mm *metricsMiddleware) RemoveChannelHandler(id string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "remove_channel").Add(1)
mm.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.RemoveChannelHandler(id)
}
func (mm *metricsMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
defer func(begin time.Time) {
mm.counter.With("method", "disconnect_thing_handler").Add(1)
mm.latency.With("method", "disconnect_thing_handler").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.DisconnectThingHandler(channelID, thingID)
}
+149
View File
@@ -0,0 +1,149 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package api
import "github.com/mainflux/mainflux/bootstrap"
type apiReq interface {
validate() error
}
type addReq struct {
key string
ThingID string `json:"thing_id"`
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key"`
Channels []string `json:"channels"`
Name string `json:"name"`
Content string `json:"content"`
}
func (req addReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.ExternalID == "" || req.ExternalKey == "" {
return bootstrap.ErrMalformedEntity
}
return nil
}
type entityReq struct {
key string
id string
}
func (req entityReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.id == "" {
return bootstrap.ErrMalformedEntity
}
return nil
}
type updateReq struct {
key string
id string
Name string `json:"name"`
Content string `json:"content"`
}
func (req updateReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.id == "" {
return bootstrap.ErrMalformedEntity
}
return nil
}
type updateConnReq struct {
key string
id string
Channels []string `json:"channels"`
}
func (req updateConnReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.id == "" {
return bootstrap.ErrMalformedEntity
}
return nil
}
type listReq struct {
key string
filter bootstrap.Filter
offset uint64
limit uint64
}
func (req listReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.limit == 0 || req.limit > maxLimit {
return bootstrap.ErrMalformedEntity
}
return nil
}
type bootstrapReq struct {
key string
id string
}
func (req bootstrapReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.id == "" {
return bootstrap.ErrMalformedEntity
}
return nil
}
type changeStateReq struct {
key string
id string
State bootstrap.State `json:"state"`
}
func (req changeStateReq) validate() error {
if req.key == "" {
return bootstrap.ErrUnauthorizedAccess
}
if req.id == "" {
return bootstrap.ErrMalformedEntity
}
if req.State != bootstrap.Inactive &&
req.State != bootstrap.Active {
return bootstrap.ErrMalformedEntity
}
return nil
}
+265
View File
@@ -0,0 +1,265 @@
package api
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/bootstrap"
"github.com/stretchr/testify/assert"
)
func TestAddReqValidation(t *testing.T) {
cases := []struct {
desc string
key string
externalID string
externalKey string
err error
}{
{
desc: "empty key",
key: "",
externalID: "external-id",
externalKey: "external-key",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty external ID",
key: "key",
externalID: "",
externalKey: "external-key",
err: bootstrap.ErrMalformedEntity,
},
{
desc: "empty external key",
key: "key",
externalID: "external-id",
externalKey: "",
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := addReq{
key: tc.key,
ExternalID: tc.externalID,
ExternalKey: tc.externalKey,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestEntityReqValidation(t *testing.T) {
cases := []struct {
desc string
key string
id string
err error
}{
{
desc: "empty key",
key: "",
id: "id",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty id",
key: "key",
id: "",
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := entityReq{
key: tc.key,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateReqValidation(t *testing.T) {
cases := []struct {
desc string
key string
id string
err error
}{
{
desc: "empty key",
key: "",
id: "id",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty id",
key: "key",
id: "",
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := updateReq{
key: tc.key,
id: tc.id,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateConnReqValidation(t *testing.T) {
cases := []struct {
desc string
key string
id string
err error
}{
{
desc: "empty key",
key: "",
id: "id",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty id",
key: "key",
id: "",
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := updateReq{
key: tc.key,
id: tc.id,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestListReqValidation(t *testing.T) {
cases := []struct {
desc string
offset uint64
key string
limit uint64
err error
}{
{
desc: "empty key",
key: "",
offset: 0,
limit: 1,
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "too large limit",
key: "key",
offset: 0,
limit: maxLimit + 1,
err: bootstrap.ErrMalformedEntity,
},
{
desc: "zero limit",
key: "key",
offset: 0,
limit: 0,
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := listReq{
key: tc.key,
offset: tc.offset,
limit: tc.limit,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestBootstrapReqValidation(t *testing.T) {
cases := []struct {
desc string
externKey string
externID string
err error
}{
{
desc: "empty external key",
externKey: "",
externID: "id",
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty external id",
externKey: "key",
externID: "",
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := bootstrapReq{
id: tc.externID,
key: tc.externKey,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestChangeStateReqValidation(t *testing.T) {
cases := []struct {
desc string
key string
id string
state bootstrap.State
err error
}{
{
desc: "empty key",
key: "",
id: "id",
state: bootstrap.State(1),
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "empty id",
key: "key",
id: "",
state: bootstrap.State(0),
err: bootstrap.ErrMalformedEntity,
},
{
desc: "invalid state",
key: "key",
id: "id",
state: bootstrap.State(14),
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
req := changeStateReq{
key: tc.key,
id: tc.id,
State: tc.state,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
+148
View File
@@ -0,0 +1,148 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package api
import (
"fmt"
"net/http"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/bootstrap"
)
var (
_ mainflux.Response = (*removeRes)(nil)
_ mainflux.Response = (*configRes)(nil)
_ mainflux.Response = (*stateRes)(nil)
_ mainflux.Response = (*viewRes)(nil)
_ mainflux.Response = (*listRes)(nil)
)
type removeRes struct{}
func (res removeRes) Code() int {
return http.StatusNoContent
}
func (res removeRes) Headers() map[string]string {
return map[string]string{}
}
func (res removeRes) Empty() bool {
return true
}
type configRes struct {
id string
created bool
}
func (res configRes) Code() int {
if res.created {
return http.StatusCreated
}
return http.StatusOK
}
func (res configRes) Headers() map[string]string {
if res.created {
return map[string]string{
"Location": fmt.Sprintf("/things/configs/%s", res.id),
}
}
return map[string]string{}
}
func (res configRes) Empty() bool {
return true
}
type channelRes struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
}
type viewRes struct {
MFThing string `json:"mainflux_id,omitempty"`
MFKey string `json:"mainflux_key,omitempty"`
Channels []channelRes `json:"mainflux_channels,omitempty"`
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key,omitempty"`
Content string `json:"content,omitempty"`
Name string `json:"name,omitempty"`
State bootstrap.State `json:"state"`
}
func (res viewRes) Code() int {
return http.StatusOK
}
func (res viewRes) Headers() map[string]string {
return map[string]string{}
}
func (res viewRes) Empty() bool {
return false
}
type unknownRes struct {
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key,omitempty"`
}
type listUnknownRes struct {
Configs []unknownRes `json:"configs"`
}
func (res listUnknownRes) Code() int {
return http.StatusOK
}
func (res listUnknownRes) Headers() map[string]string {
return map[string]string{}
}
func (res listUnknownRes) Empty() bool {
return false
}
type listRes struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
Configs []viewRes `json:"configs"`
}
func (res listRes) Code() int {
return http.StatusOK
}
func (res listRes) Headers() map[string]string {
return map[string]string{}
}
func (res listRes) Empty() bool {
return false
}
type stateRes struct{}
func (res stateRes) Code() int {
return http.StatusOK
}
func (res stateRes) Headers() map[string]string {
return map[string]string{}
}
func (res stateRes) Empty() bool {
return true
}
+332
View File
@@ -0,0 +1,332 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package api
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strconv"
"strings"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/bootstrap"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
contentType = "application/json"
maxLimit = 100
defaultLimit = 10
)
var (
errUnsupportedContentType = errors.New("unsupported content type")
errInvalidQueryParams = errors.New("invalid query params")
fullMatch = []string{"state", "external_id", "mainflux_id", "mainflux_key"}
partialMatch = []string{"name"}
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(encodeError),
}
r := bone.New()
r.Post("/things/configs", kithttp.NewServer(
addEndpoint(svc),
decodeAddRequest,
encodeResponse,
opts...))
r.Get("/things/configs/:id", kithttp.NewServer(
viewEndpoint(svc),
decodeEntityRequest,
encodeResponse,
opts...))
r.Put("/things/configs/:id", kithttp.NewServer(
updateEndpoint(svc),
decodeUpdateRequest,
encodeResponse,
opts...))
r.Put("/things/configs/connections/:id", kithttp.NewServer(
updateConnEndpoint(svc),
decodeUpdateConnRequest,
encodeResponse,
opts...))
r.Get("/things/configs", kithttp.NewServer(
listEndpoint(svc),
decodeListRequest,
encodeResponse,
opts...))
r.Get("/things/unknown/configs", kithttp.NewServer(
listEndpoint(svc),
decodeUnknownRequest,
encodeResponse,
opts...))
r.Get("/things/bootstrap/:external_id", kithttp.NewServer(
bootstrapEndpoint(svc, reader),
decodeBootstrapRequest,
encodeResponse,
opts...))
r.Put("/things/state/:id", kithttp.NewServer(
stateEndpoint(svc),
decodeStateRequest,
encodeResponse,
opts...))
r.Delete("/things/configs/:id", kithttp.NewServer(
removeEndpoint(svc),
decodeEntityRequest,
encodeResponse,
opts...))
r.GetFunc("/version", mainflux.Version("bootstrap"))
r.Handle("/metrics", promhttp.Handler())
return r
}
func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}
req := addReq{key: r.Header.Get("Authorization")}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return req, nil
}
func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}
req := updateReq{key: r.Header.Get("Authorization")}
req.id = bone.GetValue(r, "id")
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return req, nil
}
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}
req := updateConnReq{key: r.Header.Get("Authorization")}
req.id = bone.GetValue(r, "id")
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return req, nil
}
func decodeUnknownRequest(_ context.Context, r *http.Request) (interface{}, error) {
q, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return nil, errInvalidQueryParams
}
offset, limit, err := parsePagePrams(q)
if err != nil {
return nil, err
}
req := listReq{
key: r.Header.Get("Authorization"),
filter: bootstrap.Filter{Unknown: true},
offset: offset,
limit: limit,
}
return req, nil
}
func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error) {
q, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return nil, errInvalidQueryParams
}
offset, limit, err := parsePagePrams(q)
if err != nil {
return nil, err
}
filter := parseFilter(q)
req := listReq{
key: r.Header.Get("Authorization"),
filter: filter,
offset: offset,
limit: limit,
}
return req, nil
}
func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := bootstrapReq{
id: bone.GetValue(r, "external_id"),
key: r.Header.Get("Authorization"),
}
return req, nil
}
func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, errUnsupportedContentType
}
req := changeStateReq{key: r.Header.Get("Authorization")}
req.id = bone.GetValue(r, "id")
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return req, nil
}
func decodeEntityRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := entityReq{
key: r.Header.Get("Authorization"),
id: bone.GetValue(r, "id"),
}
return req, nil
}
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", contentType)
if ar, ok := response.(mainflux.Response); ok {
for k, v := range ar.Headers() {
w.Header().Set(k, v)
}
w.WriteHeader(ar.Code())
if ar.Empty() {
return nil
}
}
return json.NewEncoder(w).Encode(response)
}
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
w.Header().Set("Content-Type", contentType)
switch err {
case errUnsupportedContentType:
w.WriteHeader(http.StatusUnsupportedMediaType)
case errInvalidQueryParams, bootstrap.ErrMalformedEntity:
w.WriteHeader(http.StatusBadRequest)
case bootstrap.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
case bootstrap.ErrUnauthorizedAccess:
w.WriteHeader(http.StatusForbidden)
case bootstrap.ErrConflict:
w.WriteHeader(http.StatusConflict)
case bootstrap.ErrThings:
w.WriteHeader(http.StatusServiceUnavailable)
case io.EOF:
w.WriteHeader(http.StatusBadRequest)
default:
switch err.(type) {
case *json.SyntaxError:
w.WriteHeader(http.StatusBadRequest)
case *json.UnmarshalTypeError:
w.WriteHeader(http.StatusBadRequest)
default:
w.WriteHeader(http.StatusInternalServerError)
}
}
}
func parseUint(s string) (uint64, error) {
if s == "" {
return 0, nil
}
ret, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, errInvalidQueryParams
}
return ret, nil
}
func parsePagePrams(q url.Values) (uint64, uint64, error) {
offset, err := parseUint(q.Get("offset"))
q.Del("offset")
if err != nil {
return 0, 0, err
}
limit, err := parseUint(q.Get("limit"))
q.Del("limit")
if err != nil {
return 0, 0, err
}
if limit > maxLimit {
limit = maxLimit
}
if limit == 0 {
limit = defaultLimit
}
return offset, limit, nil
}
func parseFilter(values url.Values) bootstrap.Filter {
ret := bootstrap.Filter{
FullMatch: make(map[string]string),
PartialMatch: make(map[string]string),
}
for k := range values {
if contains(fullMatch, k) {
ret.FullMatch[k] = values.Get(k)
}
if contains(partialMatch, k) {
ret.PartialMatch[k] = strings.ToLower(values.Get(k))
}
}
return ret
}
func contains(l []string, s string) bool {
for _, v := range l {
if v == s {
return true
}
}
return false
}
+106
View File
@@ -0,0 +1,106 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package bootstrap
// Config represents Configuration entity. It wraps information about external entity
// as well as info about corresponding Mainflux entities.
// MFThing represents corresponding Mainflux Thing ID.
// MFKey is key of corresponding Mainflux Thing.
// MFChannels is a list of Mainflux Channels corresponding Mainflux Thing connects to.
type Config struct {
MFThing string
Owner string
Name string
MFKey string
MFChannels []Channel
ExternalID string
ExternalKey string
Content string
State State
}
// Channel represents Mainflux channel corresponding Mainflux Thing is connected to.
type Channel struct {
ID string
Name string
Metadata interface{}
}
// Filter is used for the search filters.
type Filter struct {
Unknown bool
FullMatch map[string]string
PartialMatch map[string]string
}
// ConfigsPage contains page related metadata as well as list of Configs that
// belong to this page.
type ConfigsPage struct {
Total uint64
Offset uint64
Limit uint64
Configs []Config
}
// ConfigRepository specifies a Config persistence API.
type ConfigRepository interface {
// Save persists the Config. Successful operation is indicated by non-nil
// error response.
Save(Config, []string) (string, error)
// RetrieveByID retrieves the Config having the provided identifier, that is owned
// by the specified user.
RetrieveByID(string, string) (Config, error)
// RetrieveAll retrieves a subset of Configs that are owned
// by the specific user, with given filter parameters.
RetrieveAll(string, Filter, uint64, uint64) ConfigsPage
// RetrieveByExternalID returns Config for given external ID.
RetrieveByExternalID(string, string) (Config, error)
// Update performs and update to an existing Config. A non-nil error is returned
// to indicate operation failure.
Update(Config) error
// UpdateConnections updates a list of Channels the Config is connected to
// adding new Channels if needed.
UpdateConnections(string, string, []Channel, []string) error
// Remove removes the Config having the provided identifier, that is owned
// by the specified user.
Remove(string, string) error
// ChangeState changes of the Config, that is owned by the specific user.
ChangeState(string, string, State) error
// SaveUnknown saves Thing which unsuccessfully bootstrapped.
SaveUnknown(string, string) error
// RetrieveUnknown returns a subset of unsuccessfully bootstrapped Things.
RetrieveUnknown(uint64, uint64) ConfigsPage
// ListExisting retrieves those channels from the given list that exist in DB.
ListExisting(string, []string) ([]Channel, error)
// Methods RemoveThing, UpdateChannel, and RemoveChannel are related to
// event sourcing. That's why these methods surpass ownership check.
// RemoveThing removes Config of the Thing with the given ID.
RemoveThing(string) error
// UpdateChannel updates channel with the given ID.
UpdateChannel(Channel) error
// RemoveChannel removes channel with the given ID.
RemoveChannel(string) error
// DisconnectHandler changes state of the Config when the corresponding Thing is
// disconnected from the Channel.
DisconnectThing(string, string) error
}
+10
View File
@@ -0,0 +1,10 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
// Package bootstrap contains the domain concept definitions needed to support
// Mainflux bootstrap service functionality.
package bootstrap
+346
View File
@@ -0,0 +1,346 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package mocks
import (
"sort"
"strconv"
"strings"
"sync"
"github.com/mainflux/mainflux/bootstrap"
)
const (
emptyState = -1
notFoundIdx = -1
)
var _ bootstrap.ConfigRepository = (*configRepositoryMock)(nil)
type configRepositoryMock struct {
mu sync.Mutex
counter uint64
configs map[string]bootstrap.Config
channels map[string]bootstrap.Channel
unknown map[string]string
}
// NewConfigsRepository creates in-memory thing repository.
func NewConfigsRepository(unknown map[string]string) bootstrap.ConfigRepository {
return &configRepositoryMock{
configs: make(map[string]bootstrap.Config),
channels: make(map[string]bootstrap.Channel),
unknown: unknown,
}
}
func (crm *configRepositoryMock) Save(config bootstrap.Config, connections []string) (string, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
for _, v := range crm.configs {
if v.MFThing == config.MFThing || v.ExternalID == config.ExternalID {
return "", bootstrap.ErrConflict
}
}
crm.counter++
config.MFThing = strconv.FormatUint(crm.counter, 10)
crm.configs[config.MFThing] = config
for _, ch := range config.MFChannels {
crm.channels[ch.ID] = ch
}
config.MFChannels = []bootstrap.Channel{}
for _, ch := range connections {
config.MFChannels = append(config.MFChannels, crm.channels[ch])
}
crm.configs[config.MFThing] = config
delete(crm.unknown, config.ExternalID)
return config.MFThing, nil
}
func (crm *configRepositoryMock) RetrieveByID(key, id string) (bootstrap.Config, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
c, ok := crm.configs[id]
if !ok {
return bootstrap.Config{}, bootstrap.ErrNotFound
}
if c.Owner != key {
return bootstrap.Config{}, bootstrap.ErrUnauthorizedAccess
}
return c, nil
}
func (crm *configRepositoryMock) RetrieveAll(key string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
crm.mu.Lock()
defer crm.mu.Unlock()
configs := make([]bootstrap.Config, 0)
if offset < 0 || limit <= 0 {
return bootstrap.ConfigsPage{}
}
first := uint64(offset) + 1
last := first + uint64(limit)
var state bootstrap.State = emptyState
var name string
if s, ok := filter.FullMatch["state"]; ok {
val, _ := strconv.Atoi(s)
state = bootstrap.State(val)
}
if s, ok := filter.PartialMatch["name"]; ok {
name = strings.ToLower(s)
}
var total uint64
for _, v := range crm.configs {
id, _ := strconv.ParseUint(v.MFThing, 10, 64)
if (state == emptyState || v.State == state) &&
(name == "" || strings.Index(strings.ToLower(v.Name), name) != notFoundIdx) &&
v.Owner == key {
if id >= first && id < last {
configs = append(configs, v)
}
total++
}
}
sort.SliceStable(configs, func(i, j int) bool {
return configs[i].MFThing < configs[j].MFThing
})
return bootstrap.ConfigsPage{
Total: total,
Offset: offset,
Limit: limit,
Configs: configs,
}
}
func (crm *configRepositoryMock) RetrieveByExternalID(externalKey, externalID string) (bootstrap.Config, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
for _, thing := range crm.configs {
if thing.ExternalID == externalID && thing.ExternalKey == externalKey {
return thing, nil
}
}
return bootstrap.Config{}, bootstrap.ErrNotFound
}
func (crm *configRepositoryMock) Update(config bootstrap.Config) error {
crm.mu.Lock()
defer crm.mu.Unlock()
cfg, ok := crm.configs[config.MFThing]
if !ok || cfg.Owner != config.Owner {
return bootstrap.ErrNotFound
}
cfg.Name = config.Name
cfg.Content = config.Content
crm.configs[config.MFThing] = cfg
return nil
}
func (crm *configRepositoryMock) UpdateConnections(key, id string, channels []bootstrap.Channel, connections []string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
config, ok := crm.configs[id]
if !ok {
return bootstrap.ErrNotFound
}
for _, ch := range channels {
crm.channels[ch.ID] = ch
}
config.MFChannels = []bootstrap.Channel{}
for _, conn := range connections {
ch, ok := crm.channels[conn]
if !ok {
return bootstrap.ErrNotFound
}
config.MFChannels = append(config.MFChannels, ch)
}
crm.configs[id] = config
return nil
}
func (crm *configRepositoryMock) Remove(key, id string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
for k, v := range crm.configs {
if v.Owner == key && k == id {
delete(crm.configs, k)
break
}
}
return nil
}
func (crm *configRepositoryMock) ChangeState(key, id string, state bootstrap.State) error {
crm.mu.Lock()
defer crm.mu.Unlock()
config, ok := crm.configs[id]
if !ok {
return bootstrap.ErrNotFound
}
if config.Owner != key {
return bootstrap.ErrUnauthorizedAccess
}
config.State = state
crm.configs[id] = config
return nil
}
func (crm *configRepositoryMock) RetrieveUnknown(offset, limit uint64) bootstrap.ConfigsPage {
crm.mu.Lock()
defer crm.mu.Unlock()
configs := []bootstrap.Config{}
i := uint64(0)
l := int(limit)
var keys []string
for k := range crm.unknown {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if i >= offset && len(configs) < l {
configs = append(configs, bootstrap.Config{
ExternalID: k,
ExternalKey: crm.unknown[k],
})
}
i++
}
return bootstrap.ConfigsPage{
Total: uint64(len(crm.unknown)),
Offset: offset,
Limit: limit,
Configs: configs,
}
}
func (crm *configRepositoryMock) RemoveUnknown(key, id string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
for k, v := range crm.unknown {
if k == id && v == key {
delete(crm.unknown, k)
return nil
}
}
return nil
}
func (crm *configRepositoryMock) SaveUnknown(key, id string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.unknown[id] = key
return nil
}
func (crm *configRepositoryMock) ListExisting(key string, connections []string) ([]bootstrap.Channel, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
var ret []bootstrap.Channel
for k, v := range crm.channels {
for _, conn := range connections {
if conn == k {
ret = append(ret, v)
break
}
}
}
return ret, nil
}
func (crm *configRepositoryMock) RemoveThing(id string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
delete(crm.configs, id)
return nil
}
func (crm *configRepositoryMock) UpdateChannel(ch bootstrap.Channel) error {
crm.mu.Lock()
defer crm.mu.Unlock()
channel, ok := crm.channels[ch.ID]
if !ok {
return nil
}
channel.Name = ch.Name
channel.Metadata = ch.Metadata
crm.channels[ch.ID] = channel
return nil
}
func (crm *configRepositoryMock) RemoveChannel(id string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
delete(crm.channels, id)
return nil
}
func (crm *configRepositoryMock) DisconnectThing(channelID, thingID string) error {
crm.mu.Lock()
defer crm.mu.Unlock()
idx := -1
if config, ok := crm.configs[thingID]; ok {
for i, ch := range config.MFChannels {
if ch.ID == channelID {
idx = i
break
}
}
if idx != -1 {
config.MFChannels = append(config.MFChannels[0:idx], config.MFChannels[idx:]...)
}
crm.configs[thingID] = config
}
delete(crm.channels, channelID)
return nil
}
+209
View File
@@ -0,0 +1,209 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package mocks
import (
"context"
"strconv"
"sync"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/things"
)
var _ things.Service = (*mainfluxThings)(nil)
type mainfluxThings struct {
mu sync.Mutex
counter uint64
things map[string]things.Thing
channels map[string]things.Channel
users mainflux.UsersServiceClient
connections map[string][]string
}
// NewThingsService returns Mainflux Things service mock.
// Only methods used by SDK are mocked.
func NewThingsService(things map[string]things.Thing, channels map[string]things.Channel, users mainflux.UsersServiceClient) things.Service {
return &mainfluxThings{
things: things,
channels: channels,
users: users,
connections: make(map[string][]string),
}
}
func (svc *mainfluxThings) AddThing(owner string, thing things.Thing) (things.Thing, error) {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return things.Thing{}, things.ErrUnauthorizedAccess
}
svc.counter++
thing.Owner = userID.Value
thing.ID = strconv.FormatUint(svc.counter, 10)
thing.Key = thing.ID
svc.things[thing.ID] = thing
return thing, nil
}
func (svc *mainfluxThings) ViewThing(owner, id string) (things.Thing, error) {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return things.Thing{}, things.ErrUnauthorizedAccess
}
if t, ok := svc.things[id]; ok && t.Owner == userID.Value {
return t, nil
}
return things.Thing{}, things.ErrNotFound
}
func (svc *mainfluxThings) Connect(owner, chanID, thingID string) error {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return things.ErrUnauthorizedAccess
}
if svc.channels[chanID].Owner != userID.Value {
return things.ErrNotFound
}
svc.connections[chanID] = append(svc.connections[chanID], thingID)
return nil
}
func (svc *mainfluxThings) Disconnect(owner, chanID, thingID string) error {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil || svc.channels[chanID].Owner != userID.Value {
return things.ErrUnauthorizedAccess
}
ids := svc.connections[chanID]
i := 0
for _, t := range ids {
if t == thingID {
break
}
i++
}
if i == len(ids) {
return things.ErrNotFound
}
var tmp []string
if i != len(ids)-2 {
tmp = ids[i+1:]
}
ids = append(ids[:i], tmp...)
svc.connections[chanID] = ids
return nil
}
func (svc *mainfluxThings) RemoveThing(owner, id string) error {
svc.mu.Lock()
defer svc.mu.Unlock()
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
if err != nil {
return things.ErrUnauthorizedAccess
}
if t, ok := svc.things[id]; !ok || t.Owner != userID.Value {
return things.ErrNotFound
}
delete(svc.things, id)
conns := make(map[string][]string)
for k, v := range svc.connections {
idx := findIndex(v, id)
if idx != -1 {
var tmp []string
if idx != len(v)-2 {
tmp = v[idx+1:]
}
conns[k] = append(v[:idx], tmp...)
}
}
svc.connections = conns
return nil
}
func (svc *mainfluxThings) ViewChannel(owner, id string) (things.Channel, error) {
if c, ok := svc.channels[id]; ok {
return c, nil
}
return things.Channel{}, things.ErrNotFound
}
func (svc *mainfluxThings) UpdateThing(string, things.Thing) error {
panic("not implemented")
}
func (svc *mainfluxThings) ListThings(string, uint64, uint64) (things.ThingsPage, error) {
panic("not implemented")
}
func (svc *mainfluxThings) ListChannelsByThing(string, string, uint64, uint64) (things.ChannelsPage, error) {
panic("not implemented")
}
func (svc *mainfluxThings) ListThingsByChannel(string, string, uint64, uint64) (things.ThingsPage, error) {
panic("not implemented")
}
func (svc *mainfluxThings) CreateChannel(string, things.Channel) (things.Channel, error) {
panic("not implemented")
}
func (svc *mainfluxThings) UpdateChannel(string, things.Channel) error {
panic("not implemented")
}
func (svc *mainfluxThings) ListChannels(string, uint64, uint64) (things.ChannelsPage, error) {
panic("not implemented")
}
func (svc *mainfluxThings) RemoveChannel(string, string) error {
panic("not implemented")
}
func (svc *mainfluxThings) CanAccess(string, string) (string, error) {
panic("not implemented")
}
func (svc *mainfluxThings) Identify(string) (string, error) {
panic("not implemented")
}
func findIndex(list []string, val string) int {
for i, v := range list {
if v == val {
return i
}
}
return -1
}
+34
View File
@@ -0,0 +1,34 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package mocks
import (
"context"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/users"
"google.golang.org/grpc"
)
var _ mainflux.UsersServiceClient = (*usersServiceMock)(nil)
type usersServiceMock struct {
users map[string]string
}
// NewUsersService creates mock of users service.
func NewUsersService(users map[string]string) mainflux.UsersServiceClient {
return &usersServiceMock{users}
}
func (svc usersServiceMock) Identify(ctx context.Context, in *mainflux.Token, opts ...grpc.CallOption) (*mainflux.UserID, error) {
if id, ok := svc.users[in.Value]; ok {
return &mainflux.UserID{Value: id}, nil
}
return nil, users.ErrUnauthorizedAccess
}
+558
View File
@@ -0,0 +1,558 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package postgres
import (
"database/sql"
"fmt"
"strings"
"github.com/lib/pq"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/logger"
)
const (
duplicateErr = "unique_violation"
uuidErr = "invalid input syntax for type uuid"
connConstraintErr = "connections_config_id_fkey"
fkViolation = "foreign_key_violation"
configFieldsNum = 8
chanFieldsNum = 3
connFieldsNum = 2
cleanupQuery = `DELETE FROM channels ch WHERE NOT EXISTS (
SELECT channel_id FROM connections c WHERE ch.mainflux_channel = c.channel_id);`
)
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
type configRepository struct {
db *sql.DB
log logger.Logger
}
// NewConfigRepository instantiates a PostgreSQL implementation of thing
// repository.
func NewConfigRepository(db *sql.DB, log logger.Logger) bootstrap.ConfigRepository {
return &configRepository{db: db, log: log}
}
func (cr configRepository) Save(cfg bootstrap.Config, connections []string) (string, error) {
q := `INSERT INTO configs (mainflux_thing, owner, name, mainflux_key, external_id, external_key, content, state)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`
content := nullString(cfg.Content)
name := nullString(cfg.Name)
tx, err := cr.db.Begin()
if err != nil {
return "", err
}
if _, err := tx.Exec(q, cfg.MFThing, cfg.Owner, name, cfg.MFKey, cfg.ExternalID, cfg.ExternalKey, content, cfg.State); err != nil {
e := err
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
e = bootstrap.ErrConflict
}
cr.rollback("Failed to insert a Config", tx, err)
return "", e
}
if err := insertChannels(cfg.Owner, cfg.MFChannels, tx); err != nil {
cr.rollback("Failed to insert Channels", tx, err)
return "", err
}
if err := insertConnections(cfg, connections, tx); err != nil {
cr.rollback("Failed to insert connections", tx, err)
return "", err
}
q = "DELETE FROM unknown_configs WHERE external_id = $1 AND external_key = $2"
if _, err := tx.Exec(q, cfg.ExternalID, cfg.ExternalKey); err != nil {
cr.rollback("Failed to remove from unknown", tx, err)
return "", err
}
if err := tx.Commit(); err != nil {
cr.rollback("Failed to commit Config save", tx, err)
}
return cfg.MFThing, nil
}
func (cr configRepository) RetrieveByID(key, id string) (bootstrap.Config, error) {
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state FROM configs WHERE mainflux_thing = $1 AND owner = $2`
cfg := bootstrap.Config{MFThing: id, Owner: key, MFChannels: []bootstrap.Channel{}}
var name, content sql.NullString
if err := cr.db.QueryRow(q, id, key).
Scan(&cfg.MFThing, &cfg.MFKey, &cfg.ExternalID, &cfg.ExternalKey, &name, &content, &cfg.State); err != nil {
empty := bootstrap.Config{}
if err == sql.ErrNoRows {
return empty, bootstrap.ErrNotFound
}
return empty, err
}
q = `SELECT mainflux_channel, name, metadata FROM channels ch
INNER JOIN connections conn
ON ch.mainflux_channel = conn.channel_id AND ch.owner = conn.config_owner
WHERE conn.config_id = $1 AND conn.config_owner = $2`
rows, err := cr.db.Query(q, cfg.MFThing, cfg.Owner)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return bootstrap.Config{}, err
}
defer rows.Close()
for rows.Next() {
c := bootstrap.Channel{}
if err := rows.Scan(&c.ID, &c.Name, &c.Metadata); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
return bootstrap.Config{}, err
}
cfg.MFChannels = append(cfg.MFChannels, c)
}
cfg.Content = content.String
cfg.Name = name.String
return cfg, nil
}
func (cr configRepository) RetrieveAll(key string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
search, params := cr.retrieveAll(key, filter)
n := len(params)
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state
FROM configs %s ORDER BY mainflux_thing LIMIT $%d OFFSET $%d`
q = fmt.Sprintf(q, search, n+1, n+2)
rows, err := cr.db.Query(q, append(params, limit, offset)...)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
return bootstrap.ConfigsPage{}
}
defer rows.Close()
var name, content sql.NullString
configs := []bootstrap.Config{}
for rows.Next() {
c := bootstrap.Config{Owner: key}
if err := rows.Scan(&c.MFThing, &c.MFKey, &c.ExternalID, &c.ExternalKey, &name, &content, &c.State); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
return bootstrap.ConfigsPage{}
}
c.Name = name.String
c.Content = content.String
configs = append(configs, c)
}
q = fmt.Sprintf(`SELECT COUNT(*) FROM configs %s`, search)
var total uint64
if err := cr.db.QueryRow(q, params...).Scan(&total); err != nil {
cr.log.Error(fmt.Sprintf("Failed to count configs due to %s", err))
return bootstrap.ConfigsPage{}
}
return bootstrap.ConfigsPage{
Total: total,
Limit: limit,
Offset: offset,
Configs: configs,
}
}
func (cr configRepository) RetrieveByExternalID(externalKey, externalID string) (bootstrap.Config, error) {
q := `SELECT mainflux_thing, mainflux_key, owner, name, content, state FROM configs WHERE external_key = $1 AND external_id = $2`
cfg := bootstrap.Config{ExternalID: externalID, ExternalKey: externalKey, MFChannels: []bootstrap.Channel{}}
var name, content sql.NullString
if err := cr.db.QueryRow(q, externalKey, externalID).
Scan(&cfg.MFThing, &cfg.MFKey, &cfg.Owner, &name, &content, &cfg.State); err != nil {
empty := bootstrap.Config{}
if err == sql.ErrNoRows {
return empty, bootstrap.ErrNotFound
}
return empty, err
}
q = `SELECT mainflux_channel, name, metadata FROM channels ch
INNER JOIN connections conn
ON ch.mainflux_channel = conn.channel_id AND ch.owner = conn.config_owner
WHERE conn.config_id = $1 AND conn.config_owner = $2`
rows, err := cr.db.Query(q, cfg.MFThing, cfg.Owner)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return bootstrap.Config{}, err
}
defer rows.Close()
for rows.Next() {
c := bootstrap.Channel{}
if err := rows.Scan(&c.ID, &c.Name, &c.Metadata); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
return bootstrap.Config{}, err
}
cfg.MFChannels = append(cfg.MFChannels, c)
}
cfg.Content = content.String
cfg.Name = name.String
return cfg, nil
}
func (cr configRepository) Update(cfg bootstrap.Config) error {
q := `UPDATE configs SET name = $1, content = $2 WHERE mainflux_thing = $3 AND owner = $4`
content := nullString(cfg.Content)
name := nullString(cfg.Name)
res, err := cr.db.Exec(q, name, content, cfg.MFThing, cfg.Owner)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
if cnt == 0 {
return bootstrap.ErrNotFound
}
return nil
}
func (cr configRepository) UpdateConnections(key, id string, channels []bootstrap.Channel, connections []string) error {
tx, err := cr.db.Begin()
if err != nil {
return err
}
if err := insertChannels(key, channels, tx); err != nil {
cr.rollback("Failed to insert Channels during the update", tx, err)
return err
}
if err := updateConnections(key, id, connections, tx); err != nil {
if e, ok := err.(*pq.Error); ok {
if e.Code.Name() == fkViolation && e.Constraint == connConstraintErr {
return bootstrap.ErrNotFound
}
}
cr.rollback("Failed to update connections during the update", tx, err)
return err
}
if err := tx.Commit(); err != nil {
cr.rollback("Failed to commit Config update", tx, err)
}
return nil
}
func (cr configRepository) Remove(key, id string) error {
q := `DELETE FROM configs WHERE mainflux_thing = $1 AND owner = $2`
if _, err := cr.db.Exec(q, id, key); err != nil {
return err
}
if _, err := cr.db.Exec(cleanupQuery); err != nil {
cr.log.Warn("Failed to clean dangling channels after removal")
}
return nil
}
func (cr configRepository) ChangeState(key, id string, state bootstrap.State) error {
q := `UPDATE configs SET state = $1 WHERE mainflux_thing = $2 AND owner = $3;`
res, err := cr.db.Exec(q, state, id, key)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
if cnt == 0 {
return bootstrap.ErrNotFound
}
return nil
}
func (cr configRepository) ListExisting(key string, ids []string) ([]bootstrap.Channel, error) {
q := "SELECT mainflux_channel, name, metadata FROM channels WHERE owner = $1 AND mainflux_channel = ANY ($2)"
rows, err := cr.db.Query(q, key, pq.Array(ids))
if err != nil {
return []bootstrap.Channel{}, err
}
var channels []bootstrap.Channel
for rows.Next() {
var ch bootstrap.Channel
if err := rows.Scan(&ch.ID, &ch.Name, &ch.Metadata); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved channels due to %s", err))
return []bootstrap.Channel{}, nil
}
channels = append(channels, ch)
}
return channels, nil
}
func (cr configRepository) SaveUnknown(key, id string) error {
q := `INSERT INTO unknown_configs (external_id, external_key) VALUES ($1, $2)`
if _, err := cr.db.Exec(q, id, key); err != nil {
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
return nil
}
return err
}
return nil
}
func (cr configRepository) RetrieveUnknown(offset, limit uint64) bootstrap.ConfigsPage {
q := `SELECT external_id, external_key FROM unknown_configs LIMIT $1 OFFSET $2`
rows, err := cr.db.Query(q, limit, offset)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve config due to %s", err))
return bootstrap.ConfigsPage{}
}
defer rows.Close()
items := []bootstrap.Config{}
for rows.Next() {
c := bootstrap.Config{}
if err := rows.Scan(&c.ExternalID, &c.ExternalKey); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
return bootstrap.ConfigsPage{}
}
items = append(items, c)
}
q = fmt.Sprintf(`SELECT COUNT(*) FROM unknown_configs`)
var total uint64
if err := cr.db.QueryRow(q).Scan(&total); err != nil {
cr.log.Error(fmt.Sprintf("Failed to count unknown configs due to %s", err))
return bootstrap.ConfigsPage{}
}
return bootstrap.ConfigsPage{
Total: total,
Offset: offset,
Limit: limit,
Configs: items,
}
}
func (cr configRepository) RemoveThing(id string) error {
q := `DELETE FROM configs WHERE mainflux_thing = $1`
_, err := cr.db.Exec(q, id)
if _, err := cr.db.Exec(cleanupQuery); err != nil {
cr.log.Warn("Failed to clean dangling channels after removal")
}
return err
}
func (cr configRepository) UpdateChannel(channel bootstrap.Channel) error {
q := `UPDATE channels SET name = $1, metadata = $2 WHERE mainflux_channel = $3`
_, err := cr.db.Exec(q, channel.Name, channel.Metadata, channel.ID)
return err
}
func (cr configRepository) RemoveChannel(id string) error {
q := `DELETE FROM channels WHERE mainflux_channel = $1`
_, err := cr.db.Exec(q, id)
return err
}
func (cr configRepository) DisconnectThing(channelID, thingID string) error {
q := `UPDATE configs SET state = $1 WHERE EXISTS (
SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
_, err := cr.db.Exec(q, bootstrap.Inactive, thingID, channelID)
return err
}
func (cr configRepository) retrieveAll(key string, filter bootstrap.Filter) (string, []interface{}) {
template := `WHERE owner = $1 %s`
params := []interface{}{key}
// One empty string so that strings Join works if only one filter is applied.
queries := []string{""}
// Since key is the first param, start from 2.
counter := 2
for k, v := range filter.FullMatch {
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
params = append(params, v)
counter++
}
for k, v := range filter.PartialMatch {
queries = append(queries, fmt.Sprintf("LOWER(%s) LIKE '%%' || $%d || '%%'", k, counter))
params = append(params, v)
counter++
}
f := strings.Join(queries, " AND ")
return fmt.Sprintf(template, f), params
}
func (cr configRepository) rollback(content string, tx *sql.Tx, err error) {
cr.log.Error(fmt.Sprintf("%s %s", content, err))
if err := tx.Rollback(); err != nil {
cr.log.Error(fmt.Sprintf("Failed to rollback due to %s", err))
}
}
func insertChannels(key string, channels []bootstrap.Channel, tx *sql.Tx) error {
if len(channels) == 0 {
return nil
}
q := `INSERT INTO channels (mainflux_channel, owner, name, metadata) VALUES `
v := []interface{}{key}
var vals []string
// Since the first value is owner, start with the second one.
count := 2
for _, ch := range channels {
vals = append(vals, fmt.Sprintf("($%d, $1, $%d, $%d)", count, count+1, count+2))
v = append(v, ch.ID, ch.Name, ch.Metadata)
count += chanFieldsNum
}
q = fmt.Sprintf("%s%s", q, strings.Join(vals, ","))
if _, err := tx.Exec(q, v...); err != nil {
e := err
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
e = bootstrap.ErrConflict
}
return e
}
return nil
}
func insertConnections(cfg bootstrap.Config, connections []string, tx *sql.Tx) error {
if len(connections) == 0 {
return nil
}
q := `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner) VALUES`
v := []interface{}{cfg.MFThing, cfg.Owner}
var vals []string
// Since the first value is Config ID and the second and third
// are Config owner, start with the second one.
count := 3
for _, id := range connections {
vals = append(vals, fmt.Sprintf("($1, $%d, $2, $2)", count))
v = append(v, id)
count++
}
q = fmt.Sprintf("%s%s", q, strings.Join(vals, ","))
_, err := tx.Exec(q, v...)
return err
}
func updateConnections(key, id string, connections []string, tx *sql.Tx) error {
if len(connections) == 0 {
return nil
}
q := `DELETE FROM connections
WHERE config_id = $1 AND config_owner = $2 AND channel_owner = $2
AND channel_id NOT IN ($3)`
v := []interface{}{id, key}
v = append(v, pq.Array(connections))
res, err := tx.Exec(q, v...)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
q = `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner) VALUES`
v = []interface{}{id, key}
var vals []string
// Since the first value is Config ID and the second is Config
// owner, start with the third one.
count := 3
for _, chID := range connections {
vals = append(vals, fmt.Sprintf("($1, $%d, $2, $2)", count))
v = append(v, chID)
count++
}
// Add connections for current list of channels. Ignore if already exists.
q = fmt.Sprintf("%s%s%s", q, strings.Join(vals, ","), "ON CONFLICT (config_id, config_owner, channel_id, channel_owner) DO NOTHING")
if _, err := tx.Exec(q, v...); err != nil {
return err
}
if cnt == 0 {
return nil
}
_, err = tx.Exec(cleanupQuery)
return err
}
func nullString(s string) sql.NullString {
if s == "" {
return sql.NullString{}
}
return sql.NullString{
String: s,
Valid: true,
}
}
+695
View File
@@ -0,0 +1,695 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package postgres_test
import (
"fmt"
"strconv"
"testing"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/bootstrap/postgres"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const numConfigs = 10
var (
config = bootstrap.Config{
MFThing: "mf-thing",
MFKey: "mf-key",
ExternalID: "external-id",
ExternalKey: "external-key",
Owner: "user@email.com",
MFChannels: []bootstrap.Channel{
bootstrap.Channel{ID: "1", Name: "name 1", Metadata: "{\"meta\":1}"},
bootstrap.Channel{ID: "2", Name: "name 2", Metadata: "{\"meta\":2}"},
},
Content: "content",
State: bootstrap.Inactive,
}
channels = []string{"1", "2"}
)
func TestSave(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
diff := "different"
duplicateThing := config
duplicateThing.ExternalID = diff
duplicateThing.MFKey = diff
duplicateThing.MFChannels = []bootstrap.Channel{}
duplicateExternal := config
duplicateExternal.MFThing = diff
duplicateExternal.MFKey = diff
duplicateExternal.MFChannels = []bootstrap.Channel{}
duplicateChannels := config
duplicateChannels.ExternalID = diff
duplicateChannels.MFKey = diff
duplicateChannels.MFThing = diff
cases := []struct {
desc string
config bootstrap.Config
connections []string
err error
}{
{
desc: "save a config",
config: config,
connections: channels,
err: nil,
},
{
desc: "save config with same Thing ID",
config: duplicateThing,
connections: nil,
err: bootstrap.ErrConflict,
},
{
desc: "save config with same external ID",
config: duplicateExternal,
connections: nil,
err: bootstrap.ErrConflict,
},
{
desc: "save config with same Channels",
config: duplicateChannels,
connections: channels,
err: bootstrap.ErrConflict,
},
}
for _, tc := range cases {
_, err := repo.Save(tc.config, tc.connections)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRetrieveByID(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
id := uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
id, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
owner string
id string
err error
}{
{
desc: "retrieve config",
owner: c.Owner,
id: id,
err: nil,
},
{
desc: "retrieve config with wrong owner",
owner: "2",
id: id,
err: bootstrap.ErrNotFound,
},
{
desc: "retrieve a non-existing config",
owner: c.Owner,
id: uuid.NewV4().String(),
err: bootstrap.ErrNotFound,
},
{
desc: "retrieve a config with invalid ID",
owner: c.Owner,
id: "invalid",
err: bootstrap.ErrNotFound,
},
}
for _, tc := range cases {
_, err := repo.RetrieveByID(tc.owner, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRetrieveAll(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
for i := 0; i < numConfigs; i++ {
c := config
// Use UUID to prevent conflict errors.
id := uuid.NewV4().String()
c.ExternalID = id
c.Name = fmt.Sprintf("name %d", i)
c.MFThing = id
c.MFKey = id
if i%2 == 0 {
c.State = bootstrap.Active
}
if i > 0 {
c.MFChannels = nil
}
_, err := repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
}
cases := []struct {
desc string
owner string
offset uint64
limit uint64
filter bootstrap.Filter
size int
}{
{
desc: "retrieve all",
owner: config.Owner,
offset: 0,
limit: uint64(numConfigs),
size: numConfigs,
},
{
desc: "retrieve subset",
owner: config.Owner,
offset: 5,
limit: uint64(numConfigs - 5),
size: numConfigs - 5,
},
{
desc: "retrieve wrong owner",
owner: "2",
offset: 0,
limit: uint64(numConfigs),
size: 0,
},
{
desc: "retrieve all active",
owner: config.Owner,
offset: 0,
limit: uint64(numConfigs),
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
size: numConfigs / 2,
},
{
desc: "retrieve search by name",
owner: config.Owner,
offset: 0,
limit: uint64(numConfigs),
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
size: 1,
},
}
for _, tc := range cases {
ret := repo.RetrieveAll(tc.owner, tc.filter, tc.offset, tc.limit)
size := len(ret.Configs)
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
}
}
func TestRetrieveByExternalID(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
id := uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
externalID string
externalKey string
err error
}{
{
desc: "retrieve with invalid external ID",
externalID: strconv.Itoa(numConfigs + 1),
externalKey: config.ExternalKey,
err: bootstrap.ErrNotFound,
},
{
desc: "retrieve with invalid external key",
externalID: c.ExternalID,
externalKey: "invalid",
err: bootstrap.ErrNotFound,
},
{
desc: "retrieve with external key",
externalID: c.ExternalID,
externalKey: c.ExternalKey,
err: nil,
},
}
for _, tc := range cases {
_, err := repo.RetrieveByExternalID(tc.externalKey, tc.externalID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdate(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
id := uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
c.Content = "new content"
c.Name = "new name"
wrongOwner := c
wrongOwner.Owner = "3"
cases := []struct {
desc string
id string
config bootstrap.Config
err error
}{
{
desc: "update with wrong owner",
config: wrongOwner,
err: bootstrap.ErrNotFound,
},
{
desc: "update a config",
config: c,
err: nil,
},
}
for _, tc := range cases {
err := repo.Update(tc.config)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateConnections(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
id := uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
// Use UUID to prevent conflicts.
id = uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
c.MFChannels = []bootstrap.Channel{}
_, err = repo.Save(c, []string{channels[0]})
require.Nil(t, err, fmt.Sprintf("Saving a config expected to succeed: %s.\n", err))
cases := []struct {
desc string
key string
id string
channels []bootstrap.Channel
connections []string
err error
}{
{
desc: "update connections of non-existing config",
key: config.Owner,
id: "unknown",
channels: nil,
connections: []string{channels[1]},
err: bootstrap.ErrNotFound,
},
{
desc: "update connections",
key: config.Owner,
id: c.MFThing,
channels: nil,
connections: []string{channels[1]},
err: nil,
},
{
desc: "update connections no channels",
key: config.Owner,
id: c.MFThing,
channels: nil,
connections: nil,
err: nil,
},
}
for _, tc := range cases {
err := repo.UpdateConnections(tc.key, tc.id, tc.channels, tc.connections)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemove(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
id := uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
id, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
// Removal works the same for both existing and non-existing
// (removed) config
for i := 0; i < 2; i++ {
err := repo.Remove(c.Owner, id)
require.Nil(t, err, fmt.Sprintf("%d: failed to remove config due to: %s", i, err))
_, err = repo.RetrieveByID(c.Owner, id)
require.Equal(t, bootstrap.ErrNotFound, err, fmt.Sprintf("%d: expected %s got %s", i, bootstrap.ErrNotFound, err))
}
}
func TestChangeState(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
id := uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
saved, err := repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
owner string
id string
state bootstrap.State
err error
}{
{
desc: "change state with wrong owner",
id: saved,
owner: "2",
err: bootstrap.ErrNotFound,
},
{
desc: "change state with wrong id",
id: uuid.NewV4().String(),
owner: c.Owner,
err: bootstrap.ErrNotFound,
},
{
desc: "change state to Active",
id: saved,
owner: c.Owner,
state: bootstrap.Active,
err: nil,
},
{
desc: "change state to Inactive",
id: saved,
owner: c.Owner,
state: bootstrap.Inactive,
err: nil,
},
}
for _, tc := range cases {
err := repo.ChangeState(tc.owner, tc.id, tc.state)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestListExisting(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
id := uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
var chs []bootstrap.Channel
for _, ch := range config.MFChannels {
ch.Metadata = []byte(ch.Metadata.(string))
chs = append(chs, ch)
}
cases := []struct {
desc string
key string
connections []string
existing []bootstrap.Channel
}{
{
desc: "list all existing channels",
key: c.Owner,
connections: channels,
existing: chs,
},
{
desc: "list a subset of existing channels",
key: c.Owner,
connections: []string{channels[0], "5"},
existing: []bootstrap.Channel{chs[0]},
},
{
desc: "list a subset of existing channels empty",
key: c.Owner,
connections: []string{"5", "6"},
existing: []bootstrap.Channel{},
},
}
for _, tc := range cases {
existing, err := repo.ListExisting(tc.key, tc.connections)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", tc.desc, err))
assert.ElementsMatch(t, tc.existing, existing, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.existing, existing))
}
}
func TestSaveUnknown(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
cases := []struct {
desc string
externalID string
externalKey string
err error
}{
{
desc: "save unknown",
externalID: uuid.NewV4().String(),
externalKey: uuid.NewV4().String(),
err: nil,
},
{
desc: "save invalid unknown",
externalID: uuid.NewV4().String(),
externalKey: "",
err: nil,
},
}
for _, tc := range cases {
err := repo.SaveUnknown(tc.externalKey, tc.externalID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRetrieveUnknown(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
for i := 0; i < numConfigs; i++ {
id := uuid.NewV4().String()
repo.SaveUnknown(id, id)
}
cases := []struct {
desc string
offset uint64
limit uint64
size int
}{
{
desc: "retrieve all",
offset: 0,
limit: uint64(numConfigs),
size: numConfigs,
},
{
desc: "retrieve a subset",
offset: 5,
limit: uint64(numConfigs - 5),
size: numConfigs - 5,
},
}
for _, tc := range cases {
ret := repo.RetrieveUnknown(tc.offset, tc.limit)
size := len(ret.Configs)
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
}
}
func TestRemoveThing(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
id := uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
saved, err := repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
for i := 0; i < 2; i++ {
err := repo.RemoveThing(saved)
assert.Nil(t, err, fmt.Sprintf("an unexpected error occured: %s\n", err))
}
}
func TestUpdateChannel(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
id := uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
id = c.MFChannels[0].ID
update := bootstrap.Channel{
ID: id,
Name: "update name",
Metadata: []byte("{\"update\":\"metadata update\"}"),
}
err = repo.UpdateChannel(update)
assert.Nil(t, err, fmt.Sprintf("updating config expected to succeed: %s.\n", err))
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
var retreved bootstrap.Channel
for _, c := range cfg.MFChannels {
if c.ID == id {
retreved = c
break
}
}
assert.Equal(t, update, retreved, fmt.Sprintf("expected %s, go %s", update, retreved))
}
func TestRemoveChannel(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
for i, ch := range c.MFChannels {
c.MFChannels[i].Metadata = []byte(ch.Metadata.(string))
}
// Use UUID to prevent conflicts.
id := uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
_, err = repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
err = repo.RemoveChannel(c.MFChannels[0].ID)
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.NotContains(t, cfg.MFChannels, c.MFChannels[0], fmt.Sprintf("expected to remove channel %s from %s", c.MFChannels[0], cfg.MFChannels))
}
func TestDisconnectThing(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
id := uuid.NewV4().String()
c.MFKey = id
c.MFThing = id
c.ExternalID = id
c.ExternalKey = id
saved, err := repo.Save(c, channels)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
err = repo.DisconnectThing(c.MFChannels[0].ID, saved)
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.Equal(t, cfg.State, bootstrap.Inactive, fmt.Sprintf("expected ti be inactive when a connection is removed from %s", cfg))
}
func deleteChannels(repo bootstrap.ConfigRepository) error {
for _, ch := range channels {
if err := repo.RemoveChannel(ch); err != nil {
return err
}
}
return nil
}
@@ -1,3 +1,10 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
// Package postgres contains repository implementations using PostgreSQL as
// the underlying database.
package postgres
+101
View File
@@ -0,0 +1,101 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package postgres
import (
"database/sql"
"fmt"
_ "github.com/lib/pq" // required for SQL access
migrate "github.com/rubenv/sql-migrate"
)
// Config defines the options that are used when connecting to a PostgreSQL instance
type Config struct {
Host string
Port string
User string
Pass string
Name string
SSLMode string
SSLCert string
SSLKey string
SSLRootCert string
}
// Connect creates a connection to the PostgreSQL instance and applies any
// unapplied database migrations. A non-nil error is returned to indicate
// failure.
func Connect(cfg Config) (*sql.DB, error) {
url := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", cfg.Host, cfg.Port, cfg.User, cfg.Name, cfg.Pass, cfg.SSLMode, cfg.SSLCert, cfg.SSLKey, cfg.SSLRootCert)
db, err := sql.Open("postgres", url)
if err != nil {
return nil, err
}
if err := migrateDB(db); err != nil {
return nil, err
}
return db, nil
}
func migrateDB(db *sql.DB) error {
migrations := &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
Id: "configs_1",
Up: []string{
`CREATE TABLE IF NOT EXISTS configs (
mainflux_thing TEXT UNIQUE NOT NULL,
owner VARCHAR(254),
name TEXT,
mainflux_key CHAR(36) UNIQUE NOT NULL,
external_id TEXT UNIQUE NOT NULL,
external_key TEXT NOT NULL,
content TEXT,
state BIGINT NOT NULL,
PRIMARY KEY (mainflux_thing, owner)
)`,
`CREATE TABLE IF NOT EXISTS unknown_configs (
external_id TEXT UNIQUE NOT NULL,
external_key TEXT NOT NULL,
PRIMARY KEY (external_id, external_key)
)`,
`CREATE TABLE IF NOT EXISTS channels (
mainflux_channel TEXT UNIQUE NOT NULL,
owner VARCHAR(254),
name TEXT,
metadata JSON,
PRIMARY KEY (mainflux_channel, owner)
)`,
`CREATE TABLE IF NOT EXISTS connections (
channel_id TEXT,
channel_owner VARCHAR(256),
config_id TEXT,
config_owner VARCHAR(256),
FOREIGN KEY (channel_id, channel_owner) REFERENCES channels (mainflux_channel, owner) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (config_id, config_owner) REFERENCES configs (mainflux_thing, owner) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (channel_id, channel_owner, config_id, config_owner)
)`,
},
Down: []string{
"DROP TABLE connections",
"DROP TABLE configs",
"DROP TABLE channels",
"DROP TABLE unknown_configs",
},
},
},
}
_, err := migrate.Exec(db, "postgres", migrations, migrate.Up)
return err
}
+85
View File
@@ -0,0 +1,85 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package postgres_test
import (
"database/sql"
"fmt"
"log"
"os"
"testing"
"github.com/mainflux/mainflux/bootstrap/postgres"
"github.com/mainflux/mainflux/logger"
"gopkg.in/ory-am/dockertest.v3"
)
const (
wrongID = "0"
wrongValue = "wrong-value"
)
var (
testLog, _ = logger.New(os.Stdout, logger.Info.String())
db *sql.DB
)
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
cfg := []string{
"POSTGRES_USER=test",
"POSTGRES_PASSWORD=test",
"POSTGRES_DB=test",
}
container, err := pool.Run("postgres", "10.2-alpine", cfg)
if err != nil {
log.Fatalf("Could not start container: %s", err)
}
port := container.GetPort("5432/tcp")
if err := pool.Retry(func() error {
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
db, err = sql.Open("postgres", url)
if err != nil {
return err
}
return db.Ping()
}); err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
dbConfig := postgres.Config{
Host: "localhost",
Port: port,
User: "test",
Pass: "test",
Name: "test",
SSLMode: "disable",
SSLCert: "",
SSLKey: "",
SSLRootCert: "",
}
if db, err = postgres.Connect(dbConfig); err != nil {
log.Fatalf("Could not setup test DB connection: %s", err)
}
defer db.Close()
code := m.Run()
if err := pool.Purge(container); err != nil {
log.Fatalf("Could not purge container: %s", err)
}
os.Exit(code)
}
+71
View File
@@ -0,0 +1,71 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package bootstrap
import (
"errors"
"net/http"
"github.com/mainflux/mainflux"
)
// bootstrapRes represent Mainflux Response to the Bootatrap request.
// This is used as a response from ConfigReader and can easily be
// replace with any other response format.
type bootstrapRes struct {
MFThing string `json:"mainflux_id"`
MFKey string `json:"mainflux_key"`
MFChannels []channelRes `json:"mainflux_channels"`
Content string `json:"content"`
}
type channelRes struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
}
func (res bootstrapRes) Code() int {
return http.StatusOK
}
func (res bootstrapRes) Headers() map[string]string {
return map[string]string{}
}
func (res bootstrapRes) Empty() bool {
return false
}
type reader struct{}
// NewConfigReader return new reader which is used to generate response
// from the config.
func NewConfigReader() ConfigReader {
return reader{}
}
func (r reader) ReadConfig(cfg Config) (mainflux.Response, error) {
if len(cfg.MFChannels) < 1 {
return bootstrapRes{}, errors.New("Invalid configuration")
}
var channels []channelRes
for _, ch := range cfg.MFChannels {
channels = append(channels, channelRes{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
}
res := bootstrapRes{
MFKey: cfg.MFKey,
MFThing: cfg.MFThing,
MFChannels: channels,
Content: cfg.Content,
}
return res, nil
}
+17
View File
@@ -0,0 +1,17 @@
package redis
type removeEvent struct {
id string
}
type updateChannelEvent struct {
id string
name string
metadata string
}
// Connection event is either connect or disconnect event.
type disconnectEvent struct {
thingID string
channelID string
}
+148
View File
@@ -0,0 +1,148 @@
package redis
import (
"fmt"
"github.com/go-redis/redis"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/logger"
)
const (
stream = "mainflux.things"
group = "mainflux.bootstrap"
thingPrefix = "thing."
thingRemove = thingPrefix + "remove"
thingDisconnect = thingPrefix + "disconnect"
channelPrefix = "channel."
channelUpdate = channelPrefix + "update"
channelRemove = channelPrefix + "remove"
exists = "BUSYGROUP Consumer Group name already exists"
)
// EventStore represents event source for things and channels provisioning.
type EventStore interface {
// Subscribes to given subject and receives events.
Subscribe(string) error
}
type eventStore struct {
svc bootstrap.Service
client *redis.Client
consumer string
logger logger.Logger
}
// NewEventStore returns new event store instance.
func NewEventStore(svc bootstrap.Service, client *redis.Client, consumer string, log logger.Logger) EventStore {
return eventStore{
svc: svc,
client: client,
consumer: consumer,
logger: log,
}
}
func (es eventStore) Subscribe(subject string) error {
err := es.client.XGroupCreateMkStream(stream, group, "$").Err()
if err != nil && err.Error() != exists {
return err
}
for {
streams, err := es.client.XReadGroup(&redis.XReadGroupArgs{
Group: group,
Consumer: es.consumer,
Streams: []string{stream, ">"},
Count: 100,
}).Result()
if err != nil || len(streams) == 0 {
continue
}
for _, msg := range streams[0].Messages {
event := msg.Values
var err error
switch event["operation"] {
case thingRemove:
rte := decodeRemoveThing(event)
err = es.handleRemoveThing(rte)
case thingDisconnect:
dte := decodeDisconnectThing(event)
err = es.handleDisconnectThing(dte)
case channelUpdate:
uce := decodeUpdateChannel(event)
err = es.handleUpdateChannel(uce)
case channelRemove:
rce := decodeRemoveChannel(event)
err = es.handleRemoveChannel(rce)
}
if err != nil {
es.logger.Warn(fmt.Sprintf("Failed to handle event sourcing: %s", err.Error()))
break
}
es.client.XAck(stream, group, msg.ID)
}
}
}
func decodeRemoveThing(event map[string]interface{}) removeEvent {
return removeEvent{
id: read(event, "id", ""),
}
}
func decodeUpdateChannel(event map[string]interface{}) updateChannelEvent {
return updateChannelEvent{
id: read(event, "id", ""),
name: read(event, "name", ""),
metadata: read(event, "metadata", ""),
}
}
func decodeRemoveChannel(event map[string]interface{}) removeEvent {
return removeEvent{
id: read(event, "id", ""),
}
}
func decodeDisconnectThing(event map[string]interface{}) disconnectEvent {
return disconnectEvent{
channelID: read(event, "chan_id", ""),
thingID: read(event, "thing_id", ""),
}
}
func (es eventStore) handleRemoveThing(rte removeEvent) error {
return es.svc.RemoveConfigHandler(rte.id)
}
func (es eventStore) handleUpdateChannel(uce updateChannelEvent) error {
channel := bootstrap.Channel{
ID: uce.id,
Name: uce.name,
Metadata: uce.metadata,
}
return es.svc.UpdateChannelHandler(channel)
}
func (es eventStore) handleRemoveChannel(rce removeEvent) error {
return es.svc.RemoveChannelHandler(rce.id)
}
func (es eventStore) handleDisconnectThing(dte disconnectEvent) error {
return es.svc.DisconnectThingHandler(dte.channelID, dte.thingID)
}
func read(event map[string]interface{}, key, def string) string {
val, ok := event[key].(string)
if !ok {
return def
}
return val
}
+454
View File
@@ -0,0 +1,454 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package bootstrap
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/mainflux/mainflux"
mfsdk "github.com/mainflux/mainflux/sdk/go"
)
const (
thingType = "device"
chanName = "channel"
)
var (
// ErrNotFound indicates a non-existent entity request.
ErrNotFound = errors.New("non-existent entity")
// ErrMalformedEntity indicates malformed entity specification.
ErrMalformedEntity = errors.New("malformed entity specification")
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
// when accessing a protected resource.
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
// ErrConflict indicates that entity with the same ID or external ID already exists.
ErrConflict = errors.New("entity already exists")
// ErrThings indicates failure to communicate with Mainflux Things service.
// It can be due to networking error or invalid/unauthorized request.
ErrThings = errors.New("error receiving response from Things service")
)
var _ Service = (*bootstrapService)(nil)
// Service specifies an API that must be fulfilled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
type Service interface {
// Add adds new Thing Config to the user identified by the provided key.
Add(string, Config) (Config, error)
// View returns Thing Config with given ID belonging to the user identified by the given key.
View(string, string) (Config, error)
// Update updates editable fields of the provided Config.
Update(string, Config) error
// UpdateConnections updates list of Channels related to given Config.
UpdateConnections(string, string, []string) error
// List returns subset of Configs with given search params that belong to the
// user identified by the given key.
List(string, Filter, uint64, uint64) (ConfigsPage, error)
// Remove removes Config with specified key that belongs to the user identified by the given key.
Remove(string, string) error
// Bootstrap returns Config to the Thing with provided external ID using external key.
Bootstrap(string, string) (Config, error)
// ChangeState changes state of the Thing with given ID and owner.
ChangeState(string, string, State) error
// Methods RemoveConfig, UpdateChannel, and RemoveChannel are used as
// handlers for events. That's why these methods surpass ownership check.
// RemoveConfigHandler removes Configuration with id received from an event.
RemoveConfigHandler(string) error
// UpdateChannelHandler updates Channel with data received from an event.
UpdateChannelHandler(Channel) error
// RemoveChannelHandler removes Channel with id received from an event.
RemoveChannelHandler(string) error
// DisconnectHandler changes state of the Config when connect/disconnect event occurs.
DisconnectThingHandler(string, string) error
}
// ConfigReader is used to parse Config into format which will be encoded
// as a JSON and consumed from the client side. The purpose of this interface
// is to provide convenient way to generate custom configuration response
// based on the specific Config which will be consumed by the client.
type ConfigReader interface {
ReadConfig(Config) (mainflux.Response, error)
}
type bootstrapService struct {
users mainflux.UsersServiceClient
configs ConfigRepository
sdk mfsdk.SDK
}
// New returns new Bootstrap service.
func New(users mainflux.UsersServiceClient, configs ConfigRepository, sdk mfsdk.SDK) Service {
return &bootstrapService{
configs: configs,
sdk: sdk,
users: users,
}
}
func (bs bootstrapService) Add(key string, cfg Config) (Config, error) {
owner, err := bs.identify(key)
if err != nil {
return Config{}, err
}
toConnect := bs.toIDList(cfg.MFChannels)
// Check if channels exist. This is the way to prevent invalid configuration to be saved.
existing, err := bs.configs.ListExisting(owner, toConnect)
if err != nil {
return Config{}, err
}
cfg.MFChannels, err = bs.connectionChannels(toConnect, bs.toIDList(existing), key)
if err != nil {
return Config{}, err
}
id := cfg.MFThing
mfThing, err := bs.thing(key, id)
if err != nil {
return Config{}, err
}
cfg.MFThing = mfThing.ID
cfg.Owner = owner
cfg.State = Inactive
cfg.MFKey = mfThing.Key
saved, err := bs.configs.Save(cfg, toConnect)
if err != nil {
if id == "" {
// Fail silently.
bs.sdk.DeleteThing(cfg.MFThing, key)
}
return Config{}, err
}
cfg.MFThing = saved
cfg.MFChannels = append(cfg.MFChannels, existing...)
return cfg, nil
}
func (bs bootstrapService) View(key, id string) (Config, error) {
owner, err := bs.identify(key)
if err != nil {
return Config{}, err
}
cfg, err := bs.configs.RetrieveByID(owner, id)
if err != nil {
return Config{}, err
}
for i, ch := range cfg.MFChannels {
if meta, ok := ch.Metadata.([]byte); ok {
if err := json.Unmarshal(meta, &cfg.MFChannels[i].Metadata); err != nil {
return Config{}, err
}
}
}
return cfg, nil
}
func (bs bootstrapService) Update(key string, cfg Config) error {
owner, err := bs.identify(key)
if err != nil {
return err
}
cfg.Owner = owner
return bs.configs.Update(cfg)
}
func (bs bootstrapService) UpdateConnections(key, id string, connections []string) error {
owner, err := bs.identify(key)
if err != nil {
return err
}
cfg, err := bs.configs.RetrieveByID(owner, id)
if err != nil {
return err
}
add, remove := bs.updateList(cfg, connections)
channels, err := bs.updateChannels(add, key)
if err != nil {
return err
}
cfg.MFChannels = channels
var connect, disconnect []string
if cfg.State == Active {
connect = add
disconnect = remove
}
for _, c := range disconnect {
if err := bs.sdk.DisconnectThing(id, c, key); err != nil {
if err == mfsdk.ErrNotFound {
continue
}
return ErrThings
}
}
for _, c := range connect {
if err := bs.sdk.ConnectThing(id, c, key); err != nil {
if err == mfsdk.ErrNotFound {
return ErrMalformedEntity
}
return ErrThings
}
}
return bs.configs.UpdateConnections(owner, id, channels, connections)
}
func (bs bootstrapService) List(key string, filter Filter, offset, limit uint64) (ConfigsPage, error) {
owner, err := bs.identify(key)
if err != nil {
return ConfigsPage{}, err
}
if filter.Unknown {
return bs.configs.RetrieveUnknown(offset, limit), nil
}
return bs.configs.RetrieveAll(owner, filter, offset, limit), nil
}
func (bs bootstrapService) Remove(key, id string) error {
owner, err := bs.identify(key)
if err != nil {
return err
}
return bs.configs.Remove(owner, id)
}
func (bs bootstrapService) Bootstrap(externalKey, externalID string) (Config, error) {
cfg, err := bs.configs.RetrieveByExternalID(externalKey, externalID)
if err != nil {
if err == ErrNotFound {
bs.configs.SaveUnknown(externalKey, externalID)
}
return Config{}, ErrNotFound
}
for i, ch := range cfg.MFChannels {
if meta, ok := ch.Metadata.([]byte); ok {
if err := json.Unmarshal(meta, &cfg.MFChannels[i].Metadata); err != nil {
return Config{}, err
}
}
}
return cfg, nil
}
func (bs bootstrapService) ChangeState(key, id string, state State) error {
owner, err := bs.identify(key)
if err != nil {
return err
}
thing, err := bs.configs.RetrieveByID(owner, id)
if err != nil {
return err
}
if thing.State == state {
return nil
}
switch state {
case Active:
for _, c := range thing.MFChannels {
if err := bs.sdk.ConnectThing(thing.MFThing, c.ID, key); err != nil {
return ErrThings
}
}
case Inactive:
for _, c := range thing.MFChannels {
if err := bs.sdk.DisconnectThing(thing.MFThing, c.ID, key); err != nil {
if err == mfsdk.ErrNotFound {
continue
}
return ErrThings
}
}
}
return bs.configs.ChangeState(owner, id, state)
}
func (bs bootstrapService) UpdateChannelHandler(channel Channel) error {
return bs.configs.UpdateChannel(channel)
}
func (bs bootstrapService) RemoveConfigHandler(id string) error {
return bs.configs.RemoveThing(id)
}
func (bs bootstrapService) RemoveChannelHandler(id string) error {
return bs.configs.RemoveChannel(id)
}
func (bs bootstrapService) DisconnectThingHandler(channelID, thingID string) error {
return bs.configs.DisconnectThing(channelID, thingID)
}
func (bs bootstrapService) identify(token string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := bs.users.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return "", ErrUnauthorizedAccess
}
return res.GetValue(), nil
}
// Method thing retrieves Mainflux Thing creating one if an empty ID is passed.
func (bs bootstrapService) thing(key, id string) (mfsdk.Thing, error) {
thingID := id
var err error
if id == "" {
thingID, err = bs.sdk.CreateThing(mfsdk.Thing{Type: thingType}, key)
if err != nil {
return mfsdk.Thing{}, err
}
}
thing, err := bs.sdk.Thing(thingID, key)
if err != nil {
if err == mfsdk.ErrNotFound {
return mfsdk.Thing{}, ErrNotFound
}
if id != "" {
bs.sdk.DeleteThing(thingID, key)
}
return mfsdk.Thing{}, ErrThings
}
return thing, nil
}
// Method updateList accepts config and channel IDs and returns three lists:
// 1) IDs of Channels to be added
// 2) IDs of Channels to be removed
// 3) IDs of common Channels for these two configs
func (bs bootstrapService) updateList(cfg Config, connections []string) (add, remove []string) {
var disconnect map[string]bool
disconnect = make(map[string]bool, len(cfg.MFChannels))
for _, c := range cfg.MFChannels {
disconnect[c.ID] = true
}
for _, c := range connections {
if disconnect[c] {
// Don't disconnect common elements.
delete(disconnect, c)
continue
}
// Connect new elements.
add = append(add, c)
}
for v := range disconnect {
remove = append(remove, v)
}
return
}
func (bs bootstrapService) toIDList(channels []Channel) []string {
var ret []string
for _, ch := range channels {
ret = append(ret, ch.ID)
}
return ret
}
func (bs bootstrapService) updateChannels(add []string, key string) ([]Channel, error) {
var ret []Channel
for _, id := range add {
ch, err := bs.sdk.Channel(id, key)
if err != nil {
return []Channel{}, ErrMalformedEntity
}
ret = append(ret, Channel{
ID: ch.ID,
Name: ch.Name,
Metadata: ch.Metadata,
})
}
return ret, nil
}
func (bs bootstrapService) connectionChannels(channels, existing []string, key string) ([]Channel, error) {
add := make(map[string]bool, len(channels))
for _, ch := range channels {
add[ch] = true
}
for _, ch := range existing {
if add[ch] == true {
delete(add, ch)
}
}
var ret []Channel
for id := range add {
ch, err := bs.sdk.Channel(id, key)
if err != nil {
return nil, ErrMalformedEntity
}
ret = append(ret, Channel{
ID: ch.ID,
Name: ch.Name,
Metadata: ch.Metadata,
})
}
return ret, nil
}
+710
View File
@@ -0,0 +1,710 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package bootstrap_test
import (
"fmt"
"net/http/httptest"
"strconv"
"testing"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/bootstrap"
"github.com/mainflux/mainflux/bootstrap/mocks"
mfsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/mainflux/mainflux/things"
httpapi "github.com/mainflux/mainflux/things/api/http"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
validToken = "validToken"
invalidToken = "invalidToken"
email = "test@example.com"
unknown = "unknown"
unknownID = "1"
unknownKey = "2"
channelsNum = 3
)
var (
channel = bootstrap.Channel{
ID: "1",
Name: "name",
Metadata: `{"name":"value"}`,
}
config = bootstrap.Config{
ExternalID: "external_id",
ExternalKey: "external_key",
MFChannels: []bootstrap.Channel{channel},
Content: "config",
}
)
func newService(users mainflux.UsersServiceClient, url string) bootstrap.Service {
things := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
config := mfsdk.Config{
BaseURL: url,
}
sdk := mfsdk.NewSDK(config)
return bootstrap.New(users, things, sdk)
}
func newThingsService(users mainflux.UsersServiceClient) things.Service {
channels := make(map[string]things.Channel, channelsNum)
for i := 0; i < channelsNum; i++ {
id := strconv.Itoa(i + 1)
channels[id] = things.Channel{
ID: id,
Owner: email,
Metadata: `{"meta":"data"}`,
}
}
return mocks.NewThingsService(map[string]things.Thing{}, channels, users)
}
func newThingsServer(svc things.Service) *httptest.Server {
mux := httpapi.MakeHandler(svc)
return httptest.NewServer(mux)
}
func TestAdd(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
neID := config
neID.MFThing = "non-existent"
wrongChannels := config
ch := channel
ch.ID = "invalid"
wrongChannels.MFChannels = append(wrongChannels.MFChannels, ch)
cases := []struct {
desc string
config bootstrap.Config
key string
err error
}{
{
desc: "add a new config",
config: config,
key: validToken,
err: nil,
},
{
desc: "add a config with an invalid ID",
config: neID,
key: validToken,
err: bootstrap.ErrNotFound,
},
{
desc: "add a config with wrong credentials",
config: config,
key: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "add a config with invalid list of channels",
config: wrongChannels,
key: validToken,
err: bootstrap.ErrMalformedEntity,
},
}
for _, tc := range cases {
_, err := svc.Add(tc.key, tc.config)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestView(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
id string
key string
err error
}{
{
desc: "view an existing config",
id: saved.MFThing,
key: validToken,
err: nil,
},
{
desc: "view a non-existing config",
id: unknown,
key: validToken,
err: bootstrap.ErrNotFound,
},
{
desc: "view a config with wrong credentials",
id: config.MFThing,
key: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
_, err := svc.View(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdate(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
c := config
ch := channel
ch.ID = "2"
c.MFChannels = append(c.MFChannels, ch)
saved, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
modifiedCreated := saved
modifiedCreated.Content = "new-config"
modifiedCreated.Name = "new name"
nonExisting := config
nonExisting.MFThing = unknown
cases := []struct {
desc string
config bootstrap.Config
key string
err error
}{
{
desc: "update a config with state Created",
config: modifiedCreated,
key: validToken,
err: nil,
},
{
desc: "update a non-existing config",
config: nonExisting,
key: validToken,
err: bootstrap.ErrNotFound,
},
{
desc: "update a config with wrong credentials",
config: saved,
key: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
err := svc.Update(tc.key, tc.config)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateConnections(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
c := config
ch := channel
ch.ID = "2"
c.MFChannels = append(c.MFChannels, ch)
created, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
c.ExternalID = uuid.NewV4().String()
active, err := svc.Add(validToken, c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
err = svc.ChangeState(validToken, active.MFThing, bootstrap.Active)
require.Nil(t, err, fmt.Sprintf("Changing state expected to succeed: %s.\n", err))
nonExisting := config
nonExisting.MFThing = unknown
cases := []struct {
desc string
key string
id string
connections []string
err error
}{
{
desc: "update connections for config with state Created",
key: validToken,
id: created.MFThing,
connections: []string{"2"},
err: nil,
},
{
desc: "update connections for config with state Active",
key: validToken,
id: active.MFThing,
connections: []string{"3"},
err: nil,
},
{
desc: "update connections for non-existing config",
key: validToken,
id: "",
connections: []string{"3"},
err: bootstrap.ErrNotFound,
},
{
desc: "update connections with invalid channels",
key: validToken,
id: created.MFThing,
connections: []string{"wrong"},
err: bootstrap.ErrMalformedEntity,
},
{
desc: "update connections a config with wrong credentials",
key: invalidToken,
id: created.MFKey,
connections: []string{"2", "3"},
err: bootstrap.ErrUnauthorizedAccess,
},
}
for _, tc := range cases {
err := svc.UpdateConnections(tc.key, tc.id, tc.connections)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestList(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
numThings := 101
var saved []bootstrap.Config
for i := 0; i < numThings; i++ {
c := config
id := uuid.NewV4().String()
c.ExternalID = id
c.ExternalKey = id
c.Name = fmt.Sprintf("%s-%d", config.Name, i)
s, err := svc.Add(validToken, c)
saved = append(saved, s)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
}
// Set one Thing to the different state
err := svc.ChangeState(validToken, "42", bootstrap.Active)
require.Nil(t, err, fmt.Sprintf("Changing config state expected to succeed: %s.\n", err))
saved[41].State = bootstrap.Active
unknownConfig := bootstrap.Config{
ExternalID: unknownID,
ExternalKey: unknownKey,
}
cases := []struct {
desc string
config bootstrap.ConfigsPage
filter bootstrap.Filter
offset uint64
limit uint64
key string
err error
}{
{
desc: "list configs",
config: bootstrap.ConfigsPage{
Total: uint64(len(saved)),
Offset: 0,
Limit: 10,
Configs: saved[0:10],
},
filter: bootstrap.Filter{},
key: validToken,
offset: 0,
limit: 10,
err: nil,
},
{
desc: "list configs with specified name",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 0,
Limit: 100,
Configs: saved[95:96],
},
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "95"}},
key: validToken,
offset: 0,
limit: 100,
err: nil,
},
{
desc: "list configs unauthorized",
config: bootstrap.ConfigsPage{},
filter: bootstrap.Filter{},
key: invalidToken,
offset: 0,
limit: 10,
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "list last page",
config: bootstrap.ConfigsPage{
Total: uint64(len(saved)),
Offset: 95,
Limit: 10,
Configs: saved[95:],
},
filter: bootstrap.Filter{},
key: validToken,
offset: 95,
limit: 10,
err: nil,
},
{
desc: "list configs with Active state",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 35,
Limit: 20,
Configs: []bootstrap.Config{saved[41]},
},
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
key: validToken,
offset: 35,
limit: 20,
err: nil,
},
{
desc: "list unknown configs",
config: bootstrap.ConfigsPage{
Total: 1,
Offset: 0,
Limit: 20,
Configs: []bootstrap.Config{unknownConfig},
},
filter: bootstrap.Filter{Unknown: true},
key: validToken,
offset: 0,
limit: 20,
err: nil,
},
}
for _, tc := range cases {
result, err := svc.List(tc.key, tc.filter, tc.offset, tc.limit)
assert.ElementsMatch(t, tc.config.Configs, result.Configs, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Configs, result.Configs))
assert.Equal(t, tc.config.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Total, result.Total))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemove(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
id string
key string
err error
}{
{
desc: "view a config with wrong credentials",
id: saved.MFThing,
key: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "remove an existing config",
id: saved.MFThing,
key: validToken,
err: nil,
},
{
desc: "remove removed config",
id: saved.MFThing,
key: validToken,
err: nil,
},
{
desc: "remove non-existing config",
id: unknown,
key: validToken,
err: nil,
},
}
for _, tc := range cases {
err := svc.Remove(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestBootstrap(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
config bootstrap.Config
externalKey string
externalID string
err error
}{
{
desc: "bootstrap using invalid external id",
config: bootstrap.Config{},
externalID: "invalid",
externalKey: saved.ExternalKey,
err: bootstrap.ErrNotFound,
},
{
desc: "bootstrap using invalid external key",
config: bootstrap.Config{},
externalID: saved.ExternalID,
externalKey: "invalid",
err: bootstrap.ErrNotFound,
},
{
desc: "bootstrap an existing config",
config: saved,
externalID: saved.ExternalID,
externalKey: saved.ExternalKey,
err: nil,
},
}
for _, tc := range cases {
config, err := svc.Bootstrap(tc.externalKey, tc.externalID)
assert.Equal(t, tc.config, config, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.config, config))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestChangeState(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
state bootstrap.State
id string
key string
err error
}{
{
desc: "change state with wrong credentials",
state: bootstrap.Active,
id: saved.MFThing,
key: invalidToken,
err: bootstrap.ErrUnauthorizedAccess,
},
{
desc: "change state of non-existing config",
state: bootstrap.Active,
id: unknown,
key: validToken,
err: bootstrap.ErrNotFound,
},
{
desc: "change state to Active",
state: bootstrap.Active,
id: saved.MFThing,
key: validToken,
err: nil,
},
{
desc: "change state to current state",
state: bootstrap.Active,
id: saved.MFThing,
key: validToken,
err: nil,
},
{
desc: "change state to Inactive",
state: bootstrap.Inactive,
id: saved.MFThing,
key: validToken,
err: nil,
},
}
for _, tc := range cases {
err := svc.ChangeState(tc.key, tc.id, tc.state)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestUpdateChannelHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
_, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
ch := bootstrap.Channel{
ID: channel.ID,
Name: "new name",
Metadata: "new meta",
}
cases := []struct {
desc string
channel bootstrap.Channel
err error
}{
{
desc: "update an existing channel",
channel: ch,
err: nil,
},
{
desc: "update a non-existing channel",
channel: bootstrap.Channel{ID: ""},
err: nil,
},
}
for _, tc := range cases {
err := svc.UpdateChannelHandler(tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemoveChannelHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
_, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
id string
err error
}{
{
desc: "remove an existing channel",
id: channel.ID,
err: nil,
},
{
desc: "remove a non-existing channel",
id: "unknown",
err: nil,
},
}
for _, tc := range cases {
err := svc.RemoveChannelHandler(tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemoveCoinfigHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
id string
err error
}{
{
desc: "remove an existing conifg",
id: saved.MFThing,
err: nil,
},
{
desc: "remove a non-existing channel",
id: "unknown",
err: nil,
},
}
for _, tc := range cases {
err := svc.RemoveConfigHandler(tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestDisconnectThingsHandler(t *testing.T) {
users := mocks.NewUsersService(map[string]string{validToken: email})
server := newThingsServer(newThingsService(users))
svc := newService(users, server.URL)
saved, err := svc.Add(validToken, config)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
thingID string
channelID string
err error
}{
{
desc: "disconnect",
channelID: channel.ID,
thingID: saved.MFThing,
err: nil,
},
{
desc: "disconnect dicsonnected",
channelID: channel.ID,
thingID: saved.MFThing,
err: nil,
},
}
for _, tc := range cases {
err := svc.DisconnectThingHandler(tc.channelID, tc.thingID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
+30
View File
@@ -0,0 +1,30 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package bootstrap
import "strconv"
const (
// Inactive Thing is created, but not able to exchange messages using Mainflux.
Inactive State = iota
// Active Thing is created, configured, and whitelisted.
Active
)
// State represents corresponding Mainflux Thing state. The possible Config States
// as well as description of what that State represents are given in the table:
// | State | What it means |
// |----------+--------------------------------------------------------------------------------|
// | Inactive | Thing is created, but isn't able to communicate over Mainflux |
// | Active | Thing is able to communicate using Mainflux |
type State int
// String returns string representation of State.
func (s State) String() string {
return strconv.Itoa(int(s))
}
+438
View File
@@ -0,0 +1,438 @@
swagger: "2.0"
info:
title: Mainflux Bootstrap service
description: HTTP API for managing platform devices and applications configuration.
version: "1.0.0"
consumes:
- "application/json"
produces:
- "application/json"
paths:
/things/configs:
post:
summary: Adds new config
description: |
Adds new config to the list of config owned by user identified using
the provided access token.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- name: config
description: JSON-formatted document describing the new config.
in: body
schema:
$ref: "#/definitions/ConfigReq"
required: true
responses:
201:
description: Config registered.
headers:
Location:
type: string
description: Created config's relative URL (i.e. /things/configs/{configId}).
400:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
get:
summary: Retrieves managed configs
description: |
Retrieves a list of managed configs. Due to performance concerns, data
is retrieved in subsets. The API configs must ensure that the entire
dataset is consumed either by making subsequent requests, or by
increasing the subset size of the initial request.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Limit"
- $ref: "#/parameters/Offset"
- $ref: "#/parameters/State"
- $ref: "#/parameters/Name"
responses:
200:
description: |
Data retrieved. Configs from this list don't contain channels.
schema:
$ref: "#/definitions/ConfigList"
400:
description: Failed due to malformed query parameters.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/things/bootstrap/{externalId}:
get:
summary: Retrieves configuration
description: |
Retrieves a configuration with given external ID and external key.
tags:
- configs
parameters:
- $ref: "#/parameters/ConfigAuthorization"
- $ref: "#/parameters/ExternalId"
responses:
200:
description: Data retrieved.
schema:
$ref: "#/definitions/BootstrapRes"
404:
description: |
Failed to retrieve corresponding config. Thing which attempted
to bootstrap is saved as an unknown Thing and can be listed and
added to the service later.
500:
$ref: "#/responses/ServiceError"
/things/configs/{configId}:
get:
summary: Retrieves config info (with channels)
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ConfigId"
responses:
200:
description: Data retrieved.
schema:
$ref: "#/definitions/ConfigRes"
403:
description: Missing or invalid access token provided.
404:
description: Config does not exist.
500:
$ref: "#/responses/ServiceError"
put:
summary: Updates config info
description: |
Update is performed by replacing the current resource data with values
provided in a request payload. Note that the owner, ID, external ID,
external key, Mainflux Thing ID and key cannot be changed.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ConfigId"
- name: config
description: JSON-formatted document describing the updated thing.
in: body
schema:
$ref: "#/definitions/ConfigUpdateReq"
required: true
responses:
200:
description: Config updated.
400:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
404:
description: Config does not exist.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
delete:
summary: Removes a Config
description: |
Removes a Config. In case of successfull removal the service will ensure
that the removed config is disconnected from all of the Maifnlux channels.
tags:
- confgis
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ConfigId"
responses:
204:
description: Config removed.
400:
description: Failed due to malformed config's ID.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/things/configs/connections/{configId}:
put:
summary: Updates channels the thing is connected to
description: |
Update connections performs update of the channel list corresponding
Thing is connected to.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ConfigId"
- name: channels
description: Array if IDs the thing is be connected to.
in: body
schema:
type: array
items:
type: string
required: true
responses:
200:
description: Config updated.
400:
description: Failed due to malformed JSON.
403:
description: Missing or invalid access token provided.
404:
description: Config does not exist.
415:
description: Missing or invalid content type.
500:
$ref: "#/responses/ServiceError"
/things/state/{configId}:
put:
summary: Updates Config state.
description: |
Updating state represents enabling/disabling Config, i.e. connecting
and disconnecting corresponding Mainflux Thing to the list of Channels.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/ConfigId"
- name: state
description: New state of the Config.
in: body
schema:
type: object
properties:
state:
type: integer
enum:
- inactive
- active
responses:
204:
description: Config removed.
400:
description: Failed due to malformed config's ID.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
/things/unknown/configs:
get:
summary: Get a list of unsucessfully bootstrapped Things
description: |
Retrieves a list of unknown configs. Due to performance concerns, data
is retrieved in subsets. The API configs must ensure that the entire
dataset is consumed either by making subsequent requests, or by
increasing the subset size of the initial request.
tags:
- configs
parameters:
- $ref: "#/parameters/Authorization"
- $ref: "#/parameters/Limit"
- $ref: "#/parameters/Offset"
responses:
200:
description: Data retrieved.
schema:
$ref: "#/definitions/ConfigList"
400:
description: Failed due to malformed query parameters.
403:
description: Missing or invalid access token provided.
500:
$ref: "#/responses/ServiceError"
parameters:
Authorization:
name: Authorization
description: User's access token.
in: header
type: string
required: true
ConfigAuthorization:
name: configAuthorization
description: Configuration external key.
in: header
type: string
required: true
ConfigId:
name: configId
description: Unique Config identifier.
in: path
type: integer
minimum: 1
required: true
ExternalId:
name: externalId
description: Unique Config identifier provided by external entity.
in: path
type: string
required: true
Limit:
name: limit
description: Size of the subset to retrieve.
in: query
type: integer
default: 10
maximum: 100
minimum: 1
required: false
Offset:
name: offset
description: Number of items to skip during retrieval.
in: query
type: integer
default: 0
minimum: 0
required: false
State:
name: state
description: A state of items
in: query
type: integer
enum:
- inactive
- active
required: false
Name:
name: name
description: Name of the config. Search by name is partial-match and case-insensitive.
in: query
type: string
required: false
responses:
ServiceError:
description: Unexpected server-side error occured.
definitions:
ConfigList:
type: object
properties:
total:
type: integer
description: Total number of results.
minimum: 0
offset:
type: integer
description: Number of items to skip during retrieval.
minimum: 0
default: 0
limit:
type: integer
description: Size of the subset to retrieve.
maximum: 100
default: 10
configs:
type: array
minItems: 0
uniqueItems: true
items:
$ref: "#/definitions/ConfigRes"
required:
- confgis
State:
type: integer
enum:
- active
- inactive
ConfigRes:
type: object
properties:
mainflux_thing:
type: string
description: Corresponding Mainflux Thing ID.
mainflux_key:
type: string
description: Corresponding Mainflux Thing key.
mainflux_channels:
type: array
minItems: 0
items:
type: object
properties:
id:
type: string
description: ID of the Channel.
name:
type: string
description: Name of the Channel.
metadata:
type: object
description: Custom metadata related to the Channel.
external_id:
type: string
description: External ID (MAC address or some uinque identifier).
external_key:
type: string
description: External key.
content:
type: string
description: Free-form custom configuration
state:
$ref: '#/definitions/State'
required:
- external_id
- external_key
BootstrapRes:
type: object
properties:
mainflux_thing:
type: string
description: Corresponding Mainflux Thing ID.
mainflux_key:
type: string
description: Corresponding Mainflux Thing key.
mainflux_channels:
type: array
minItems: 0
items:
type: string
content:
type: string
description: Free-form custom configuration
required:
- mainflux_thing
- mainflux_key
- mainflux_channels
- content
ConfigReq:
type: object
properties:
external_id:
type: string
description: External ID (MAC address or some uinque identifier).
external_key:
type: string
description: External key.
thing_id:
type: string
description: ID of the corresponding Mainflux Thing.
channels:
type: array
minItems: 0
items:
type: string
content:
type: string
required:
- external_id
- external_key
ConfigUpdateReq:
type: object
properties:
channels:
type: array
minItems: 0
items:
type: string
content:
type: string
name:
type: string
required:
- content
- name
+108
View File
@@ -0,0 +1,108 @@
# Mainflux CLI
## Build
From the project root:
```
make cli
```
## Usage
### Service
#### Get the version of Mainflux services
```
mainflux-cli version
```
### Users management
#### Create User
```
mainflux-cli users create john.doe@email.com password
```
#### Login User
```
mainflux-cli users token john.doe@email.com password
```
### System Provisioning
#### Create Thing (type Device)
```
mainflux-cli things create '{"type":"device", "name":"myDevice"}' <user_auth_token>
```
#### Create Thing (type Application)
```
mainflux-cli things create '{"type":"app", "name":"myDevice"}' <user_auth_token>
```
#### Update Thing
```
mainflux-cli things update '{"id":"<thing_id>", "name":"myNewName"}' <user_auth_token>
```
#### Remove Thing
```
mainflux-cli things delete <thing_id> <user_auth_token>
```
#### Retrieve a subset list of provisioned Things
```
mainflux-cli things get all --offset=1 --limit=5 <user_auth_token>
```
#### Retrieve Thing By ID
```
mainflux-cli things get <thing_id> <user_auth_token>
```
#### Create Channel
```
mainflux-cli channels create '{"name":"myChannel"}' <user_auth_token>
```
#### Update Channel
```
mainflux-cli channels update '{"id":"<channel_id>","name":"myNewName"}' <user_auth_token>
```
#### Remove Channel
```
mainflux-cli channels delete <channel_id> <user_auth_token>
```
#### Retrieve a subset list of provisioned Channels
```
mainflux-cli channels get all --offset=1 --limit=5 <user_auth_token>
```
#### Retrieve Channel By ID
```
mainflux-cli channels get <channel_id> <user_auth_token>
```
### Access control
#### Connect Thing to Channel
```
mainflux-cli things connect <thing_id> <channel_id> <user_auth_token>
```
#### Disconnect Thing from Channel
```
mainflux-cli things disconnect <thing_id> <channel_id> <user_auth_token>
```
#### Retrieve a subset list of Channels connected to Thing
```
mainflux-cli things connections <thing_id> <user_auth_token>
```
#### Retrieve a subset list of Things connected to Channel
```
mainflux-cli channels connections <channel_id> <user_auth_token>
```
### Messaging
#### Send a message over HTTP
```
mainflux-cli msg send <channel_id> '[{"bn":"Dev1","n":"temp","v":20}, {"n":"hum","v":40}, {"bn":"Dev2", "n":"temp","v":20}, {"n":"hum","v":40}]' <thing_auth_token>
```
+152
View File
@@ -0,0 +1,152 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package cli
import (
"encoding/json"
mfxsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/spf13/cobra"
)
var cmdChannels = []cobra.Command{
cobra.Command{
Use: "create",
Short: "create <JSON_channel> <user_auth_token>",
Long: `Creates new channel and generates it's UUID`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var channel mfxsdk.Channel
if err := json.Unmarshal([]byte(args[0]), &channel); err != nil {
logError(err)
return
}
id, err := sdk.CreateChannel(channel, args[1])
if err != nil {
logError(err)
return
}
logCreated(id)
},
},
cobra.Command{
Use: "get",
Short: "get [all | <channel_id>] <user_auth_token>",
Long: `Gets list of all channels or gets channel by id`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
if args[0] == "all" {
l, err := sdk.Channels(args[1], uint64(Offset), uint64(Limit))
if err != nil {
logError(err)
return
}
logJSON(l)
return
}
c, err := sdk.Channel(args[0], args[1])
if err != nil {
logError(err)
return
}
logJSON(c)
},
},
cobra.Command{
Use: "update",
Short: "update <JSON_string> <user_auth_token>",
Long: `Updates channel record`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var channel mfxsdk.Channel
if err := json.Unmarshal([]byte(args[0]), &channel); err != nil {
logError(err)
return
}
if err := sdk.UpdateChannel(channel, args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "delete",
Short: "delete <channel_id> <user_auth_token>",
Long: `Delete channel by ID`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
if err := sdk.DeleteChannel(args[0], args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "connections",
Short: "connections <channel_id> <user_auth_token>",
Long: `List of Things connected to Channel`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
cl, err := sdk.ThingsByChannel(args[1], args[0], uint64(Offset), uint64(Limit))
if err != nil {
logError(err)
return
}
logJSON(cl)
},
},
}
// NewChannelsCmd returns channels command.
func NewChannelsCmd() *cobra.Command {
cmd := cobra.Command{
Use: "channels",
Short: "Channels management",
Long: `Channels management: create, get, update or delete Channel and get list of Things connected to Channel`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("channels [create | get | update | delete | connections]")
},
}
for i := range cmdChannels {
cmd.AddCommand(&cmdChannels[i])
}
return &cmd
}
+70
View File
@@ -0,0 +1,70 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package cli
import "github.com/spf13/cobra"
const contentTypeSenml = "application/senml+json"
var cmdMessages = []cobra.Command{
cobra.Command{
Use: "send",
Short: "send <channel_id>[.<subtopic>...] <JSON_string> <thing_key>",
Long: `Sends message on the channel`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsage(cmd.Short)
return
}
if err := sdk.SendMessage(args[0], args[1], args[2]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "read",
Short: "read <channel_id>[.<subtopic>...] <thing_key>",
Long: `Reads all channel messages`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
m, err := sdk.ReadMessages(args[0], args[1])
if err != nil {
logError(err)
return
}
logJSON(m)
},
},
}
// NewMessagesCmd returns messages command.
func NewMessagesCmd() *cobra.Command {
cmd := cobra.Command{
Use: "messages",
Short: "Send or read messages",
Long: `Send or read messages using the http-adapter and the configured database reader`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("messages [send | read]")
},
}
for i := range cmdMessages {
cmd.AddCommand(&cmdMessages[i])
}
return &cmd
}
+251
View File
@@ -0,0 +1,251 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package cli
import (
"bufio"
"encoding/csv"
"errors"
"fmt"
"io"
"os"
"github.com/docker/docker/pkg/namesgenerator"
mfxsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/spf13/cobra"
)
var errMalformedCSV = errors.New("malformed CSV")
func createThing(name, kind, token string) (mfxsdk.Thing, error) {
id, err := sdk.CreateThing(mfxsdk.Thing{Name: name, Type: kind}, token)
if err != nil {
return mfxsdk.Thing{}, err
}
t, err := sdk.Thing(id, token)
if err != nil {
return mfxsdk.Thing{}, err
}
m := mfxsdk.Thing{
ID: id,
Name: name,
Type: kind,
Key: t.Key,
}
return m, nil
}
func createChannel(name, token string) (mfxsdk.Channel, error) {
id, err := sdk.CreateChannel(mfxsdk.Channel{Name: name}, token)
if err != nil {
return mfxsdk.Channel{}, nil
}
c := mfxsdk.Channel{
ID: id,
Name: name,
}
return c, nil
}
var cmdProvision = []cobra.Command{
cobra.Command{
Use: "things",
Short: "things <things_csv> <user_token>",
Long: `Provisions things`,
Run: func(cmd *cobra.Command, args []string) {
things := []mfxsdk.Thing{}
if len(args) != 2 {
logUsage(cmd.Short)
return
}
c, err := os.Open(args[0])
if err != nil {
logError(err)
return
}
reader := csv.NewReader(bufio.NewReader(c))
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
logError(err)
return
}
if len(l) < 2 {
logError(errMalformedCSV)
return
}
m, err := createThing(l[0], l[1], args[1])
if err != nil {
logError(err)
return
}
things = append(things, m)
}
logJSON(things)
},
},
cobra.Command{
Use: "channels",
Short: "channels <channels_csv> <user_token>",
Long: `Provisions channels`,
Run: func(cmd *cobra.Command, args []string) {
channels := []mfxsdk.Channel{}
if len(args) != 2 {
logUsage(cmd.Short)
return
}
c, err := os.Open(args[0])
if err != nil {
logError(err)
return
}
reader := csv.NewReader(bufio.NewReader(c))
for {
l, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
logError(err)
return
}
if len(l) < 1 {
logError(errMalformedCSV)
return
}
c, err := createChannel(l[0], args[1])
if err != nil {
logError(err)
return
}
channels = append(channels, c)
}
logJSON(channels)
},
},
cobra.Command{
Use: "test",
Short: "test",
Long: `Provisions test setup: one test user, two things and two channels. \
Connect both things to one of the channels, \
and only on thing to other channel.`,
Run: func(cmd *cobra.Command, args []string) {
numThings := 2
numChan := 2
things := []mfxsdk.Thing{}
channels := []mfxsdk.Channel{}
if len(args) != 0 {
logUsage(cmd.Short)
return
}
un := fmt.Sprintf("%s@email.com", namesgenerator.GetRandomName(0))
// Create test user
user := mfxsdk.User{
Email: un,
Password: "123",
}
if err := sdk.CreateUser(user); err != nil {
logError(err)
return
}
ut, err := sdk.CreateToken(user)
if err != nil {
logError(err)
return
}
// Create things
for i := 0; i < numThings; i++ {
n := fmt.Sprintf("d%d", i)
k := "device"
if i%2 != 0 {
k = "app"
}
m, err := createThing(n, k, ut)
if err != nil {
logError(err)
return
}
things = append(things, m)
}
// Create channels
for i := 0; i < numChan; i++ {
n := fmt.Sprintf("c%d", i)
c, err := createChannel(n, ut)
if err != nil {
logError(err)
return
}
channels = append(channels, c)
}
// Connect things to channels - first thing to both channels, second only to first
for i := 0; i < numThings; i++ {
if err := sdk.ConnectThing(things[i].ID, channels[i].ID, ut); err != nil {
logError(err)
return
}
if i%2 == 0 {
if i+1 >= len(channels) {
break
}
if err := sdk.ConnectThing(things[i].ID, channels[i+1].ID, ut); err != nil {
logError(err)
return
}
}
}
logJSON(user, ut, things, channels)
},
},
}
// NewProvisionCmd returns provision command.
func NewProvisionCmd() *cobra.Command {
cmd := cobra.Command{
Use: "provision",
Short: "Provision things and channels from config file",
Long: `Provision things and channels: use csv config file to provision things and channels`,
}
for i := range cmdProvision {
cmd.AddCommand(&cmdProvision[i])
}
return &cmd
}
+18
View File
@@ -0,0 +1,18 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package cli
import mfxsdk "github.com/mainflux/mainflux/sdk/go"
// Keep SDK handle in global var
var sdk mfxsdk.SDK
// SetSDK sets mainflux SDK instance.
func SetSDK(s mfxsdk.SDK) {
sdk = s
}
+189
View File
@@ -0,0 +1,189 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package cli
import (
"encoding/json"
mfxsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/spf13/cobra"
)
const thingsEP = "things"
var cmdThings = []cobra.Command{
cobra.Command{
Use: "create",
Short: "create <JSON_thing> <user_auth_token>",
Long: `Create new thing, generate his UUID and store it`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var thing mfxsdk.Thing
if err := json.Unmarshal([]byte(args[0]), &thing); err != nil {
logError(err)
return
}
id, err := sdk.CreateThing(thing, args[1])
if err != nil {
logError(err)
return
}
logCreated(id)
},
},
cobra.Command{
Use: "get",
Short: "get [all | <thing_id>] <user_auth_token>",
Long: `Get all things or thing by id`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
if args[0] == "all" {
l, err := sdk.Things(args[1], uint64(Offset), uint64(Limit))
if err != nil {
logError(err)
return
}
logJSON(l)
return
}
t, err := sdk.Thing(args[0], args[1])
if err != nil {
logError(err)
return
}
logJSON(t)
},
},
cobra.Command{
Use: "delete",
Short: "delete <thing_id> <user_auth_token>",
Long: `Removes thing from database`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
if err := sdk.DeleteThing(args[0], args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "update",
Short: "update <JSON_string> <user_auth_token>",
Long: `Update thing record`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
var thing mfxsdk.Thing
if err := json.Unmarshal([]byte(args[0]), &thing); err != nil {
logError(err)
return
}
if err := sdk.UpdateThing(thing, args[1]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "connect",
Short: "connect <thing_id> <channel_id> <user_auth_token>",
Long: `Connect thing to the channel`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsage(cmd.Short)
return
}
if err := sdk.ConnectThing(args[0], args[1], args[2]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "disconnect",
Short: "disconnect <thing_id> <channel_id> <user_auth_token>",
Long: `Disconnect thing to the channel`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsage(cmd.Short)
return
}
if err := sdk.DisconnectThing(args[0], args[1], args[2]); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "connections",
Short: "connections <thing_id> <user_auth_token>",
Long: `List of Channels connected to Thing`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
cl, err := sdk.ChannelsByThing(args[1], args[0], uint64(Offset), uint64(Limit))
if err != nil {
logError(err)
return
}
logJSON(cl)
},
},
}
// NewThingsCmd returns things command.
func NewThingsCmd() *cobra.Command {
cmd := cobra.Command{
Use: "things",
Short: "Things management",
Long: `Things management: create, get, update or delete Thing, connect or disconnect Thing from Channel and get the list of Channels connected to Thing`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("things [create | get | update | delete | connect | disconnect | connections]")
},
}
for i := range cmdThings {
cmd.AddCommand(&cmdThings[i])
}
return &cmd
}
+79
View File
@@ -0,0 +1,79 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package cli
import (
mfxsdk "github.com/mainflux/mainflux/sdk/go"
"github.com/spf13/cobra"
)
var cmdUsers = []cobra.Command{
cobra.Command{
Use: "create",
Short: "create <username> <password>",
Long: `Creates new user`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
user := mfxsdk.User{
Email: args[0],
Password: args[1],
}
if err := sdk.CreateUser(user); err != nil {
logError(err)
return
}
logOK()
},
},
cobra.Command{
Use: "token",
Short: "token <username> <password>",
Long: `Creates new token`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsage(cmd.Short)
return
}
user := mfxsdk.User{
Email: args[0],
Password: args[1],
}
token, err := sdk.CreateToken(user)
if err != nil {
logError(err)
return
}
logCreated(token)
},
},
}
// NewUsersCmd returns users command.
func NewUsersCmd() *cobra.Command {
cmd := cobra.Command{
Use: "users",
Short: "Users management",
Long: `Users management: create accounts and tokens"`,
Run: func(cmd *cobra.Command, args []string) {
logUsage("Usage: users [create | token]")
},
}
for i := range cmdUsers {
cmd.AddCommand(&cmdUsers[i])
}
return &cmd
}
+57
View File
@@ -0,0 +1,57 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package cli
import (
"encoding/json"
"fmt"
"github.com/fatih/color"
prettyjson "github.com/hokaccha/go-prettyjson"
)
var (
// Limit query parameter
Limit uint = 10
// Offset query parameter
Offset uint
)
func logJSON(iList ...interface{}) {
for _, i := range iList {
m, err := json.Marshal(i)
if err != nil {
logError(err)
return
}
pj, err := prettyjson.Format(m)
if err != nil {
logError(err)
return
}
fmt.Printf("\n%s\n\n", string(pj))
}
}
func logUsage(u string) {
fmt.Printf(color.YellowString("\nusage: %s\n\n"), u)
}
func logError(err error) {
fmt.Printf("\n%s\n\n", color.RedString(err.Error()))
}
func logOK() {
fmt.Printf("\n%s\n\n", color.BlueString("ok"))
}
func logCreated(e string) {
fmt.Printf(color.BlueString("\ncreated: %s\n\n"), e)
}
+28
View File
@@ -0,0 +1,28 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package cli
import "github.com/spf13/cobra"
// NewVersionCmd returns version command.
func NewVersionCmd() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Mainflux services version",
Long: `Mainflux services version: get version of Mainflux Things Service`,
Run: func(cmd *cobra.Command, args []string) {
v, err := sdk.Version()
if err != nil {
logError(err)
return
}
logJSON(v)
},
}
}
+266
View File
@@ -0,0 +1,266 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
r "github.com/go-redis/redis"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/bootstrap"
api "github.com/mainflux/mainflux/bootstrap/api"
"github.com/mainflux/mainflux/bootstrap/postgres"
"github.com/mainflux/mainflux/bootstrap/redis"
"github.com/mainflux/mainflux/logger"
mfsdk "github.com/mainflux/mainflux/sdk/go"
usersapi "github.com/mainflux/mainflux/users/api/grpc"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
defLogLevel = "error"
defDBHost = "localhost"
defDBPort = "5432"
defDBUser = "mainflux"
defDBPass = "mainflux"
defDBName = "bootstrap"
defDBSSLMode = "disable"
defDBSSLCert = ""
defDBSSLKey = ""
defDBSSLRootCert = ""
defClientTLS = "false"
defCACerts = ""
defPort = "8180"
defServerCert = ""
defServerKey = ""
defBaseURL = "http://localhost"
defThingsPrefix = ""
defUsersURL = "localhost:8181"
defESURL = "localhost:6379"
defESPass = ""
defESDB = "0"
defInstanceName = "bootstrap"
envLogLevel = "MF_BOOTSTRAP_LOG_LEVEL"
envDBHost = "MF_BOOTSTRAP_DB_HOST"
envDBPort = "MF_BOOTSTRAP_DB_PORT"
envDBUser = "MF_BOOTSTRAP_DB_USER"
envDBPass = "MF_BOOTSTRAP_DB_PASS"
envDBName = "MF_BOOTSTRAP_DB"
envDBSSLMode = "MF_BOOTSTRAP_DB_SSL_MODE"
envDBSSLCert = "MF_BOOTSTRAP_DB_SSL_CERT"
envDBSSLKey = "MF_BOOTSTRAP_DB_SSL_KEY"
envDBSSLRootCert = "MF_BOOTSTRAP_DB_SSL_ROOT_CERT"
envClientTLS = "MF_BOOTSTRAP_CLIENT_TLS"
envCACerts = "MF_BOOTSTRAP_CA_CERTS"
envPort = "MF_BOOTSTRAP_PORT"
envServerCert = "MF_BOOTSTRAP_SERVER_CERT"
envServerKey = "MF_BOOTSTRAP_SERVER_KEY"
envBaseURL = "MF_SDK_BASE_URL"
envThingsPrefix = "MF_SDK_THINGS_PREFIX"
envUsersURL = "MF_USERS_URL"
envESURL = "MF_THINGS_ES_URL"
envESPass = "MF_THINGS_ES_PASS"
envESDB = "MF_THINGS_ES_DB"
envInstanceName = "MF_BOOTSTRAP_INSTANCE_NAME"
)
type config struct {
logLevel string
dbConfig postgres.Config
clientTLS bool
caCerts string
httpPort string
serverCert string
serverKey string
baseURL string
thingsPrefix string
usersURL string
esURL string
esPass string
esDB string
instanceName string
}
func main() {
cfg := loadConfig()
logger, err := logger.New(os.Stdout, cfg.logLevel)
if err != nil {
log.Fatalf(err.Error())
}
db := connectToDB(cfg.dbConfig, logger)
defer db.Close()
conn := connectToUsers(cfg, logger)
defer conn.Close()
esConn := connectToRedis(cfg.esURL, cfg.esPass, cfg.esDB, logger)
defer esConn.Close()
svc := newService(conn, db, logger, cfg)
errs := make(chan error, 2)
go startHTTPServer(svc, cfg, logger, errs)
go subscribeToThingsES(svc, esConn, cfg.instanceName, logger)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("Bootstrap service terminated: %s", err))
}
func loadConfig() config {
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
if err != nil {
tls = false
}
dbConfig := postgres.Config{
Host: mainflux.Env(envDBHost, defDBHost),
Port: mainflux.Env(envDBPort, defDBPort),
User: mainflux.Env(envDBUser, defDBUser),
Pass: mainflux.Env(envDBPass, defDBPass),
Name: mainflux.Env(envDBName, defDBName),
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
}
return config{
logLevel: mainflux.Env(envLogLevel, defLogLevel),
dbConfig: dbConfig,
clientTLS: tls,
caCerts: mainflux.Env(envCACerts, defCACerts),
httpPort: mainflux.Env(envPort, defPort),
serverCert: mainflux.Env(envServerCert, defServerCert),
serverKey: mainflux.Env(envServerKey, defServerKey),
baseURL: mainflux.Env(envBaseURL, defBaseURL),
thingsPrefix: mainflux.Env(envThingsPrefix, defThingsPrefix),
usersURL: mainflux.Env(envUsersURL, defUsersURL),
esURL: mainflux.Env(envESURL, defESURL),
esPass: mainflux.Env(envESPass, defESPass),
esDB: mainflux.Env(envESDB, defESDB),
instanceName: mainflux.Env(envInstanceName, defInstanceName),
}
}
func connectToDB(cfg postgres.Config, logger logger.Logger) *sql.DB {
db, err := postgres.Connect(cfg)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
os.Exit(1)
}
return db
}
func connectToRedis(redisURL, redisPass, redisDB string, logger logger.Logger) *r.Client {
db, err := strconv.Atoi(redisDB)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to redis: %s", err))
os.Exit(1)
}
return r.NewClient(&r.Options{
Addr: redisURL,
Password: redisPass,
DB: db,
})
}
func newService(conn *grpc.ClientConn, db *sql.DB, logger logger.Logger, cfg config) bootstrap.Service {
thingsRepo := postgres.NewConfigRepository(db, logger)
config := mfsdk.Config{
BaseURL: cfg.baseURL,
ThingsPrefix: cfg.thingsPrefix,
}
sdk := mfsdk.NewSDK(config)
users := usersapi.NewClient(conn)
svc := bootstrap.New(users, thingsRepo, sdk)
svc = api.NewLoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "bootstrap",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "bootstrap",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
return svc
}
func connectToUsers(cfg config, logger logger.Logger) *grpc.ClientConn {
var opts []grpc.DialOption
if cfg.clientTLS {
if cfg.caCerts != "" {
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
if err != nil {
logger.Error(fmt.Sprintf("Failed to create tls credentials: %s", err))
os.Exit(1)
}
opts = append(opts, grpc.WithTransportCredentials(tpc))
}
} else {
opts = append(opts, grpc.WithInsecure())
logger.Info("gRPC communication is not encrypted")
}
conn, err := grpc.Dial(cfg.usersURL, opts...)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
os.Exit(1)
}
return conn
}
func startHTTPServer(svc bootstrap.Service, cfg config, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", cfg.httpPort)
if cfg.serverCert != "" || cfg.serverKey != "" {
logger.Info(fmt.Sprintf("Bootstrap service started using https on port %s with cert %s key %s",
cfg.httpPort, cfg.serverCert, cfg.serverKey))
errs <- http.ListenAndServeTLS(p, cfg.serverCert, cfg.serverKey, api.MakeHandler(svc, bootstrap.NewConfigReader()))
return
}
logger.Info(fmt.Sprintf("Bootstrap service started using http on port %s", cfg.httpPort))
errs <- http.ListenAndServe(p, api.MakeHandler(svc, bootstrap.NewConfigReader()))
}
func subscribeToThingsES(svc bootstrap.Service, client *r.Client, consumer string, logger logger.Logger) {
eventStore := redis.NewEventStore(svc, client, consumer, logger)
logger.Info("Subscribed to Redis Event Store")
if err := eventStore.Subscribe("mainflux.things"); err != nil {
logger.Warn(fmt.Sprintf("Botstrap service failed to subscribe to event sourcing: %s", err))
}
}
+171
View File
@@ -0,0 +1,171 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/gocql/gocql"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/readers"
"github.com/mainflux/mainflux/readers/api"
"github.com/mainflux/mainflux/readers/cassandra"
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
sep = ","
defLogLevel = "error"
defPort = "8180"
defCluster = "127.0.0.1"
defKeyspace = "mainflux"
defThingsURL = "localhost:8181"
defClientTLS = "false"
defCACerts = ""
envLogLevel = "MF_CASSANDRA_READER_LOG_LEVEL"
envPort = "MF_CASSANDRA_READER_PORT"
envCluster = "MF_CASSANDRA_READER_DB_CLUSTER"
envKeyspace = "MF_CASSANDRA_READER_DB_KEYSPACE"
envThingsURL = "MF_THINGS_URL"
envClientTLS = "MF_CASSANDRA_READER_CLIENT_TLS"
envCACerts = "MF_CASSANDRA_READER_CA_CERTS"
)
type config struct {
logLevel string
port string
cluster string
keyspace string
thingsURL string
clientTLS bool
caCerts string
}
func main() {
cfg := loadConfig()
logger, err := logger.New(os.Stdout, cfg.logLevel)
if err != nil {
log.Fatalf(err.Error())
}
session := connectToCassandra(cfg.cluster, cfg.keyspace, logger)
defer session.Close()
conn := connectToThings(cfg, logger)
defer conn.Close()
tc := thingsapi.NewClient(conn)
repo := newService(session, logger)
errs := make(chan error, 2)
go startHTTPServer(repo, tc, cfg.port, errs, logger)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("Cassandra reader service terminated: %s", err))
}
func loadConfig() config {
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
if err != nil {
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
}
return config{
logLevel: mainflux.Env(envLogLevel, defLogLevel),
port: mainflux.Env(envPort, defPort),
cluster: mainflux.Env(envCluster, defCluster),
keyspace: mainflux.Env(envKeyspace, defKeyspace),
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
clientTLS: tls,
caCerts: mainflux.Env(envCACerts, defCACerts),
}
}
func connectToCassandra(cluster, keyspace string, logger logger.Logger) *gocql.Session {
session, err := cassandra.Connect(strings.Split(cluster, sep), keyspace)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to Cassandra cluster: %s", err))
os.Exit(1)
}
return session
}
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
var opts []grpc.DialOption
if cfg.clientTLS {
if cfg.caCerts != "" {
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
if err != nil {
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
os.Exit(1)
}
opts = append(opts, grpc.WithTransportCredentials(tpc))
}
} else {
logger.Info("gRPC communication is not encrypted")
opts = append(opts, grpc.WithInsecure())
}
conn, err := grpc.Dial(cfg.thingsURL, opts...)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
os.Exit(1)
}
return conn
}
func newService(session *gocql.Session, logger logger.Logger) readers.MessageRepository {
repo := cassandra.New(session)
repo = api.LoggingMiddleware(repo, logger)
repo = api.MetricsMiddleware(
repo,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "cassandra",
Subsystem: "message_reader",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "cassandra",
Subsystem: "message_reader",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
return repo
}
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, port string, errs chan error, logger logger.Logger) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("Cassandra reader service started, exposed port %s", port))
errs <- http.ListenAndServe(p, api.MakeHandler(repo, tc, "cassandra-reader"))
}
+143
View File
@@ -0,0 +1,143 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/gocql/gocql"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/writers"
"github.com/mainflux/mainflux/writers/cassandra"
"github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
queue = "cassandra-writer"
sep = ","
defNatsURL = nats.DefaultURL
defLogLevel = "error"
defPort = "8180"
defCluster = "127.0.0.1"
defKeyspace = "mainflux"
envNatsURL = "MF_NATS_URL"
envLogLevel = "MF_CASSANDRA_WRITER_LOG_LEVEL"
envPort = "MF_CASSANDRA_WRITER_PORT"
envCluster = "MF_CASSANDRA_WRITER_DB_CLUSTER"
envKeyspace = "MF_CASSANDRA_WRITER_DB_KEYSPACE"
)
type config struct {
natsURL string
logLevel string
port string
cluster string
keyspace string
}
func main() {
cfg := loadConfig()
logger, err := logger.New(os.Stdout, cfg.logLevel)
if err != nil {
log.Fatalf(err.Error())
}
nc := connectToNATS(cfg.natsURL, logger)
defer nc.Close()
session := connectToCassandra(cfg.cluster, cfg.keyspace, logger)
defer session.Close()
repo := newService(session, logger)
if err := writers.Start(nc, repo, queue, logger); err != nil {
logger.Error(fmt.Sprintf("Failed to create Cassandra writer: %s", err))
}
errs := make(chan error, 2)
go startHTTPServer(cfg.port, errs, logger)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("Cassandra writer service terminated: %s", err))
}
func loadConfig() config {
return config{
natsURL: mainflux.Env(envNatsURL, defNatsURL),
logLevel: mainflux.Env(envLogLevel, defLogLevel),
port: mainflux.Env(envPort, defPort),
cluster: mainflux.Env(envCluster, defCluster),
keyspace: mainflux.Env(envKeyspace, defKeyspace),
}
}
func connectToNATS(url string, logger logger.Logger) *nats.Conn {
nc, err := nats.Connect(url)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
return nc
}
func connectToCassandra(cluster, keyspace string, logger logger.Logger) *gocql.Session {
session, err := cassandra.Connect(strings.Split(cluster, sep), keyspace)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to Cassandra cluster: %s", err))
os.Exit(1)
}
return session
}
func newService(session *gocql.Session, logger logger.Logger) writers.MessageRepository {
repo := cassandra.New(session)
repo = writers.LoggingMiddleware(repo, logger)
repo = writers.MetricsMiddleware(
repo,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "cassandra",
Subsystem: "message_writer",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "cassandra",
Subsystem: "message_writer",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
return repo
}
func startHTTPServer(port string, errs chan error, logger logger.Logger) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("Cassandra writer service started, exposed port %s", port))
errs <- http.ListenAndServe(p, cassandra.MakeHandler())
}
+126
View File
@@ -0,0 +1,126 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"log"
"github.com/mainflux/mainflux/cli"
"github.com/mainflux/mainflux/sdk/go"
"github.com/spf13/cobra"
)
func main() {
msgContentType := string(sdk.CTJSONSenML)
sdkConf := sdk.Config{
BaseURL: "http://localhost",
ReaderURL: "http://localhost:8905",
ReaderPrefix: "",
UsersPrefix: "",
ThingsPrefix: "",
HTTPAdapterPrefix: "http",
MsgContentType: sdk.ContentType(msgContentType),
TLSVerification: false,
}
// Root
var rootCmd = &cobra.Command{
Use: "mainflux-cli",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
sdkConf.MsgContentType = sdk.ContentType(msgContentType)
s := sdk.NewSDK(sdkConf)
cli.SetSDK(s)
},
}
// API commands
versionCmd := cli.NewVersionCmd()
usersCmd := cli.NewUsersCmd()
thingsCmd := cli.NewThingsCmd()
channelsCmd := cli.NewChannelsCmd()
messagesCmd := cli.NewMessagesCmd()
provisionCmd := cli.NewProvisionCmd()
// Root Commands
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(usersCmd)
rootCmd.AddCommand(thingsCmd)
rootCmd.AddCommand(channelsCmd)
rootCmd.AddCommand(messagesCmd)
rootCmd.AddCommand(provisionCmd)
// Root Flags
rootCmd.PersistentFlags().StringVarP(
&sdkConf.BaseURL,
"mainflux-url",
"m",
sdkConf.BaseURL,
"Mainflux host URL",
)
rootCmd.PersistentFlags().StringVarP(
&sdkConf.UsersPrefix,
"users-prefix",
"u",
sdkConf.UsersPrefix,
"Mainflux users service prefix",
)
rootCmd.PersistentFlags().StringVarP(
&sdkConf.ThingsPrefix,
"things-prefix",
"t",
sdkConf.ThingsPrefix,
"Mainflux things service prefix",
)
rootCmd.PersistentFlags().StringVarP(
&sdkConf.HTTPAdapterPrefix,
"http-prefix",
"a",
sdkConf.HTTPAdapterPrefix,
"Mainflux http adapter prefix",
)
rootCmd.PersistentFlags().StringVarP(
&msgContentType,
"content-type",
"c",
msgContentType,
"Mainflux message content type",
)
rootCmd.PersistentFlags().BoolVarP(
&sdkConf.TLSVerification,
"insecure",
"i",
sdkConf.TLSVerification,
"Do not check for TLS cert",
)
// Client and Channels Flags
rootCmd.PersistentFlags().UintVarP(
&cli.Limit,
"limit",
"l",
100,
"limit query parameter",
)
rootCmd.PersistentFlags().UintVarP(
&cli.Offset,
"offset",
"o",
0,
"offset query parameter",
)
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
+135 -40
View File
@@ -1,50 +1,109 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
gocoap "github.com/dustin/go-coap"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap"
"github.com/mainflux/mainflux/coap/api"
"github.com/mainflux/mainflux/coap/nats"
logger "github.com/mainflux/mainflux/logger"
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
broker "github.com/nats-io/go-nats"
"go.uber.org/zap"
)
const (
port int = 5683
defNatsURL string = broker.DefaultURL
envNatsURL string = "COAP_ADAPTER_NATS_URL"
defPort = "5683"
defNatsURL = broker.DefaultURL
defThingsURL = "localhost:8181"
defLogLevel = "error"
defClientTLS = "false"
defCACerts = ""
defPingPeriod = "12"
envPort = "MF_COAP_ADAPTER_PORT"
envNatsURL = "MF_NATS_URL"
envThingsURL = "MF_THINGS_URL"
envLogLevel = "MF_COAP_ADAPTER_LOG_LEVEL"
envClientTLS = "MF_COAP_ADAPTER_CLIENT_TLS"
envCACerts = "MF_COAP_ADAPTER_CA_CERTS"
envPingPeriod = "MF_COAP_ADAPTER_PING_PERIOD"
)
type config struct {
Port int
NatsURL string
port string
natsURL string
thingsURL string
logLevel string
clientTLS bool
caCerts string
pingPeriod time.Duration
}
func main() {
cfg := loadConfig()
logger, _ := zap.NewProduction()
defer logger.Sync() // flushes buffer, if any
logger, err := logger.New(os.Stdout, cfg.logLevel)
if err != nil {
log.Fatalf(err.Error())
}
nc := connectToNats(cfg, logger)
nc, err := broker.Connect(cfg.natsURL)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
pub := nats.NewMessagePublisher(nc)
ca := adapter.NewCoAPAdapter(logger, pub)
conn := connectToThings(cfg, logger)
defer conn.Close()
nc.Subscribe("src.http", ca.BridgeHandler)
nc.Subscribe("src.mqtt", ca.BridgeHandler)
cc := thingsapi.NewClient(conn)
respChan := make(chan string, 10000)
pubsub := nats.New(nc)
svc := coap.New(pubsub, respChan)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "coap_adapter",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "coap_adapter",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
errs := make(chan error, 2)
go func() {
coapAddr := fmt.Sprintf(":%d", cfg.Port)
errs <- ca.Serve(coapAddr)
}()
go startHTTPServer(cfg.port, logger, errs)
go startCOAPServer(cfg, svc, cc, respChan, logger, errs)
go func() {
c := make(chan os.Signal)
@@ -52,32 +111,68 @@ func main() {
errs <- fmt.Errorf("%s", <-c)
}()
c := <-errs
logger.Info("terminated", zap.String("error", c.Error()))
err = <-errs
logger.Error(fmt.Sprintf("CoAP adapter terminated: %s", err))
}
func loadConfig() *config {
return &config{
NatsURL: env(envNatsURL, defNatsURL),
Port: port,
}
}
func env(key, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}
func connectToNats(cfg *config, logger *zap.Logger) *broker.Conn {
nc, err := broker.Connect(cfg.NatsURL)
func loadConfig() config {
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
if err != nil {
logger.Error("Failed to connect to NATS", zap.Error(err))
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
}
pp, err := strconv.ParseInt(mainflux.Env(envPingPeriod, defPingPeriod), 10, 64)
if err != nil {
log.Fatalf("Invalid value passed for %s\n", envPingPeriod)
}
if pp < 1 || pp > 24 {
log.Fatalf("Value of %s must be between 1 and 24", envPingPeriod)
}
return config{
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
natsURL: mainflux.Env(envNatsURL, defNatsURL),
port: mainflux.Env(envPort, defPort),
logLevel: mainflux.Env(envLogLevel, defLogLevel),
clientTLS: tls,
caCerts: mainflux.Env(envCACerts, defCACerts),
pingPeriod: time.Duration(pp),
}
}
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
var opts []grpc.DialOption
if cfg.clientTLS {
if cfg.caCerts != "" {
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
if err != nil {
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
os.Exit(1)
}
opts = append(opts, grpc.WithTransportCredentials(tpc))
}
} else {
logger.Info("gRPC communication is not encrypted")
opts = append(opts, grpc.WithInsecure())
}
conn, err := grpc.Dial(cfg.thingsURL, opts...)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
os.Exit(1)
}
return nc
return conn
}
func startHTTPServer(port string, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("CoAP service started, exposed port %s", port))
errs <- http.ListenAndServe(p, api.MakeHTTPHandler())
}
func startCOAPServer(cfg config, svc coap.Service, auth mainflux.ThingsServiceClient, respChan chan<- string, l logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", cfg.port)
l.Info(fmt.Sprintf("CoAP adapter service started, exposed port %s", cfg.port))
errs <- gocoap.ListenAndServe("udp", p, api.MakeCOAPHandler(svc, auth, l, respChan, cfg.pingPeriod))
}
+86 -22
View File
@@ -1,54 +1,79 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"google.golang.org/grpc/credentials"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
adapter "github.com/mainflux/mainflux/http"
"github.com/mainflux/mainflux/http/api"
"github.com/mainflux/mainflux/http/nats"
log "github.com/mainflux/mainflux/logger"
manager "github.com/mainflux/mainflux/manager/client"
"github.com/mainflux/mainflux/logger"
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
broker "github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
)
const (
defPort string = "8180"
defNatsURL string = broker.DefaultURL
defManagerURL string = "http://localhost:8180"
envPort string = "MF_HTTP_ADAPTER_PORT"
envNatsURL string = "MF_NATS_URL"
envManagerURL string = "MF_MANAGER_URL"
defClientTLS = "false"
defCACerts = ""
defPort = "8180"
defLogLevel = "error"
defNatsURL = broker.DefaultURL
defThingsURL = "localhost:8181"
envClientTLS = "MF_HTTP_ADAPTER_CLIENT_TLS"
envCACerts = "MF_HTTP_ADAPTER_CA_CERTS"
envPort = "MF_HTTP_ADAPTER_PORT"
envLogLevel = "MF_HTTP_ADAPTER_LOG_LEVEL"
envNatsURL = "MF_NATS_URL"
envThingsURL = "MF_THINGS_URL"
)
type config struct {
ManagerURL string
NatsURL string
Port string
thingsURL string
natsURL string
logLevel string
port string
clientTLS bool
caCerts string
}
func main() {
cfg := config{
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
cfg := loadConfig()
logger, err := logger.New(os.Stdout, cfg.logLevel)
if err != nil {
log.Fatalf(err.Error())
}
logger := log.New(os.Stdout)
nc, err := broker.Connect(cfg.NatsURL)
nc, err := broker.Connect(cfg.natsURL)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
conn := connectToThings(cfg, logger)
defer conn.Close()
cc := thingsapi.NewClient(conn)
pub := nats.NewMessagePublisher(nc)
svc := adapter.New(pub)
@@ -72,10 +97,9 @@ func main() {
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%s", cfg.Port)
mc := manager.NewClient(cfg.ManagerURL)
logger.Info(fmt.Sprintf("HTTP adapter service started, exposed port %s", cfg.Port))
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc))
p := fmt.Sprintf(":%s", cfg.port)
logger.Info(fmt.Sprintf("HTTP adapter service started on port %s", cfg.port))
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc))
}()
go func() {
@@ -87,3 +111,43 @@ func main() {
err = <-errs
logger.Error(fmt.Sprintf("HTTP adapter terminated: %s", err))
}
func loadConfig() config {
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
if err != nil {
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
}
return config{
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
natsURL: mainflux.Env(envNatsURL, defNatsURL),
logLevel: mainflux.Env(envLogLevel, defLogLevel),
port: mainflux.Env(envPort, defPort),
clientTLS: tls,
caCerts: mainflux.Env(envCACerts, defCACerts),
}
}
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
var opts []grpc.DialOption
if cfg.clientTLS {
if cfg.caCerts != "" {
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
if err != nil {
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
os.Exit(1)
}
opts = append(opts, grpc.WithTransportCredentials(tpc))
}
} else {
logger.Info("gRPC communication is not encrypted")
opts = append(opts, grpc.WithInsecure())
}
conn, err := grpc.Dial(cfg.thingsURL, opts...)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
os.Exit(1)
}
return conn
}
+178
View File
@@ -0,0 +1,178 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
influxdata "github.com/influxdata/influxdb/client/v2"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/readers"
"github.com/mainflux/mainflux/readers/api"
"github.com/mainflux/mainflux/readers/influxdb"
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
defThingsURL = "localhost:8181"
defLogLevel = "error"
defPort = "8180"
defDBName = "mainflux"
defDBHost = "localhost"
defDBPort = "8086"
defDBUser = "mainflux"
defDBPass = "mainflux"
defClientTLS = "false"
defCACerts = ""
envThingsURL = "MF_THINGS_URL"
envLogLevel = "MF_INFLUX_READER_LOG_LEVEL"
envPort = "MF_INFLUX_READER_PORT"
envDBName = "MF_INFLUX_READER_DB_NAME"
envDBHost = "MF_INFLUX_READER_DB_HOST"
envDBPort = "MF_INFLUX_READER_DB_PORT"
envDBUser = "MF_INFLUX_READER_DB_USER"
envDBPass = "MF_INFLUX_READER_DB_PASS"
envClientTLS = "MF_INFLUX_READER_CLIENT_TLS"
envCACerts = "MF_INFLUX_READER_CA_CERTS"
)
type config struct {
thingsURL string
logLevel string
port string
dbName string
dbHost string
dbPort string
dbUser string
dbPass string
clientTLS bool
caCerts string
}
func main() {
cfg, clientCfg := loadConfigs()
logger, err := logger.New(os.Stdout, cfg.logLevel)
if err != nil {
log.Fatalf(err.Error())
}
conn := connectToThings(cfg, logger)
defer conn.Close()
tc := thingsapi.NewClient(conn)
client, err := influxdata.NewHTTPClient(clientCfg)
if err != nil {
logger.Error(fmt.Sprintf("Failed to create InfluxDB client: %s", err))
os.Exit(1)
}
defer client.Close()
repo := newService(client, cfg.dbName, logger)
errs := make(chan error, 2)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
go startHTTPServer(repo, tc, cfg.port, logger, errs)
err = <-errs
logger.Error(fmt.Sprintf("InfluxDB writer service terminated: %s", err))
}
func loadConfigs() (config, influxdata.HTTPConfig) {
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
if err != nil {
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
}
cfg := config{
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
logLevel: mainflux.Env(envLogLevel, defLogLevel),
port: mainflux.Env(envPort, defPort),
dbName: mainflux.Env(envDBName, defDBName),
dbHost: mainflux.Env(envDBHost, defDBHost),
dbPort: mainflux.Env(envDBPort, defDBPort),
dbUser: mainflux.Env(envDBUser, defDBUser),
dbPass: mainflux.Env(envDBPass, defDBPass),
clientTLS: tls,
caCerts: mainflux.Env(envCACerts, defCACerts),
}
clientCfg := influxdata.HTTPConfig{
Addr: fmt.Sprintf("http://%s:%s", cfg.dbHost, cfg.dbPort),
Username: cfg.dbUser,
Password: cfg.dbPass,
}
return cfg, clientCfg
}
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
var opts []grpc.DialOption
if cfg.clientTLS {
if cfg.caCerts != "" {
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
if err != nil {
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
os.Exit(1)
}
opts = append(opts, grpc.WithTransportCredentials(tpc))
}
} else {
logger.Info("gRPC communication is not encrypted")
opts = append(opts, grpc.WithInsecure())
}
conn, err := grpc.Dial(cfg.thingsURL, opts...)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
os.Exit(1)
}
return conn
}
func newService(client influxdata.Client, dbName string, logger logger.Logger) readers.MessageRepository {
repo, err := influxdb.New(client, dbName)
if err != nil {
logger.Error(fmt.Sprintf("Failed to create InfluxDB writer: %s", err))
os.Exit(1)
}
repo = api.LoggingMiddleware(repo, logger)
repo = api.MetricsMiddleware(
repo,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "influxdb",
Subsystem: "message_reader",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "influxdb",
Subsystem: "message_reader",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
return repo
}
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, port string, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("InfluxDB reader service started, exposed port %s", port))
errs <- http.ListenAndServe(p, api.MakeHandler(repo, tc, "influxdb-reader"))
}
+174
View File
@@ -0,0 +1,174 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
influxdata "github.com/influxdata/influxdb/client/v2"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/writers"
"github.com/mainflux/mainflux/writers/influxdb"
"github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
queue = "influxdb-writer"
defNatsURL = nats.DefaultURL
defLogLevel = "error"
defPort = "8180"
defBatchSize = "5000"
defBatchTimeout = "5"
defDBName = "mainflux"
defDBHost = "localhost"
defDBPort = "8086"
defDBUser = "mainflux"
defDBPass = "mainflux"
envNatsURL = "MF_NATS_URL"
envLogLevel = "MF_INFLUX_WRITER_LOG_LEVEL"
envPort = "MF_INFLUX_WRITER_PORT"
envBatchSize = "MF_INFLUX_WRITER_BATCH_SIZE"
envBatchTimeout = "MF_INFLUX_WRITER_BATCH_TIMEOUT"
envDBName = "MF_INFLUX_WRITER_DB_NAME"
envDBHost = "MF_INFLUX_WRITER_DB_HOST"
envDBPort = "MF_INFLUX_WRITER_DB_PORT"
envDBUser = "MF_INFLUX_WRITER_DB_USER"
envDBPass = "MF_INFLUX_WRITER_DB_PASS"
)
type config struct {
NatsURL string
LogLevel string
Port string
BatchSize string
BatchTimeout string
DBName string
DBHost string
DBPort string
DBUser string
DBPass string
}
func main() {
cfg, clientCfg := loadConfigs()
logger, err := logger.New(os.Stdout, cfg.LogLevel)
if err != nil {
log.Fatalf(err.Error())
}
nc, err := nats.Connect(cfg.NatsURL)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
client, err := influxdata.NewHTTPClient(clientCfg)
if err != nil {
logger.Error(fmt.Sprintf("Failed to create InfluxDB client: %s", err))
os.Exit(1)
}
defer client.Close()
batchTimeout, err := strconv.Atoi(cfg.BatchTimeout)
if err != nil {
logger.Error(fmt.Sprintf("Invalid value for batch timeout: %s", err))
os.Exit(1)
}
batchSize, err := strconv.Atoi(cfg.BatchSize)
if err != nil {
logger.Error(fmt.Sprintf("Invalid value of batch size: %s", err))
os.Exit(1)
}
timeout := time.Duration(batchTimeout) * time.Second
repo, err := influxdb.New(client, cfg.DBName, batchSize, timeout)
if err != nil {
logger.Error(fmt.Sprintf("Failed to create InfluxDB writer: %s", err))
os.Exit(1)
}
counter, latency := makeMetrics()
repo = writers.LoggingMiddleware(repo, logger)
repo = writers.MetricsMiddleware(repo, counter, latency)
if err := writers.Start(nc, repo, queue, logger); err != nil {
logger.Error(fmt.Sprintf("Failed to start message writer: %s", err))
os.Exit(1)
}
errs := make(chan error, 2)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
go startHTTPService(cfg.Port, logger, errs)
err = <-errs
logger.Error(fmt.Sprintf("InfluxDB writer service terminated: %s", err))
}
func loadConfigs() (config, influxdata.HTTPConfig) {
cfg := config{
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
LogLevel: mainflux.Env(envLogLevel, defLogLevel),
Port: mainflux.Env(envPort, defPort),
BatchSize: mainflux.Env(envBatchSize, defBatchSize),
BatchTimeout: mainflux.Env(envBatchTimeout, defBatchTimeout),
DBName: mainflux.Env(envDBName, defDBName),
DBHost: mainflux.Env(envDBHost, defDBHost),
DBPort: mainflux.Env(envDBPort, defDBPort),
DBUser: mainflux.Env(envDBUser, defDBUser),
DBPass: mainflux.Env(envDBPass, defDBPass),
}
clientCfg := influxdata.HTTPConfig{
Addr: fmt.Sprintf("http://%s:%s", cfg.DBHost, cfg.DBPort),
Username: cfg.DBUser,
Password: cfg.DBPass,
}
return cfg, clientCfg
}
func makeMetrics() (*kitprometheus.Counter, *kitprometheus.Summary) {
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "influxdb",
Subsystem: "message_writer",
Name: "request_count",
Help: "Number of database inserts.",
}, []string{"method"})
latency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "influxdb",
Subsystem: "message_writer",
Name: "request_latency_microseconds",
Help: "Total duration of inserts in microseconds.",
}, []string{"method"})
return counter, latency
}
func startHTTPService(port string, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("InfluxDB writer service started, exposed port %s", p))
errs <- http.ListenAndServe(p, influxdb.MakeHandler())
}
+220
View File
@@ -0,0 +1,220 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
mqtt "github.com/eclipse/paho.mqtt.golang"
r "github.com/go-redis/redis"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/lora"
"github.com/mainflux/mainflux/lora/api"
pub "github.com/mainflux/mainflux/lora/nats"
mqttBroker "github.com/mainflux/mainflux/lora/paho"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux/lora/redis"
"github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
defHTTPPort = "8180"
defLoraMsgURL = "tcp://localhost:1883"
defNatsURL = nats.DefaultURL
defLogLevel = "error"
defESURL = "localhost:6379"
defESPass = ""
defESDB = "0"
defInstanceName = "lora"
defRouteMapURL = "localhost:6379"
defRouteMapPass = ""
defRouteMapDB = "0"
envHTTPPort = "MF_LORA_ADAPTER_HTTP_PORT"
envLoraMsgURL = "MF_LORA_ADAPTER_MESSAGES_URL"
envNatsURL = "MF_NATS_URL"
envLogLevel = "MF_LORA_ADAPTER_LOG_LEVEL"
envESURL = "MF_THINGS_ES_URL"
envESPass = "MF_THINGS_ES_PASS"
envESDB = "MF_THINGS_ES_DB"
envInstanceName = "MF_LORA_ADAPTER_INSTANCE_NAME"
envRouteMapURL = "MF_LORA_ADAPTER_ROUTEMAP_URL"
envRouteMapPass = "MF_LORA_ADAPTER_ROUTEMAP_PASS"
envRouteMapDB = "MF_LORA_ADAPTER_ROUTEMAP_DB"
loraServerTopic = "application/+/device/+/rx"
thingsRMPrefix = "thing"
channelsRMPrefix = "channel"
)
type config struct {
httpPort string
loraMsgURL string
natsURL string
logLevel string
esURL string
esPass string
esDB string
instanceName string
routeMapURL string
routeMapPass string
routeMapDB string
}
func main() {
cfg := loadConfig()
logger, err := logger.New(os.Stdout, cfg.logLevel)
if err != nil {
log.Fatalf(err.Error())
}
natsConn := connectToNATS(cfg.natsURL, logger)
defer natsConn.Close()
rmConn := connectToRedis(cfg.routeMapURL, cfg.routeMapPass, cfg.routeMapDB, logger)
defer rmConn.Close()
esConn := connectToRedis(cfg.esURL, cfg.esPass, cfg.esDB, logger)
defer esConn.Close()
publisher := pub.NewMessagePublisher(natsConn)
thingRM := newRouteMapRepositoy(rmConn, thingsRMPrefix, logger)
chanRM := newRouteMapRepositoy(rmConn, channelsRMPrefix, logger)
mqttConn := connectToMQTTBroker(cfg.loraMsgURL, logger)
svc := lora.New(publisher, thingRM, chanRM)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "lora_adapter",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "lora_adapter",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
go subscribeToLoRaBroker(svc, mqttConn, logger)
go subscribeToThingsES(svc, esConn, cfg.instanceName, logger)
errs := make(chan error, 2)
go startHTTPServer(cfg, logger, errs)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("LoRa adapter terminated: %s", err))
}
func loadConfig() config {
return config{
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
loraMsgURL: mainflux.Env(envLoraMsgURL, defLoraMsgURL),
natsURL: mainflux.Env(envNatsURL, defNatsURL),
logLevel: mainflux.Env(envLogLevel, defLogLevel),
esURL: mainflux.Env(envESURL, defESURL),
esPass: mainflux.Env(envESPass, defESPass),
esDB: mainflux.Env(envESDB, defESDB),
instanceName: mainflux.Env(envInstanceName, defInstanceName),
routeMapURL: mainflux.Env(envRouteMapURL, defRouteMapURL),
routeMapPass: mainflux.Env(envRouteMapPass, defRouteMapPass),
routeMapDB: mainflux.Env(envRouteMapDB, defRouteMapDB),
}
}
func connectToNATS(url string, logger logger.Logger) *nats.Conn {
conn, err := nats.Connect(url)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
logger.Info("Connected to NATS")
return conn
}
func connectToMQTTBroker(loraURL string, logger logger.Logger) mqtt.Client {
opts := mqtt.NewClientOptions()
opts.AddBroker(loraURL)
opts.SetUsername("")
opts.SetPassword("")
opts.SetOnConnectHandler(func(c mqtt.Client) {
logger.Info("Connected to Lora MQTT broker")
})
opts.SetConnectionLostHandler(func(c mqtt.Client, err error) {
logger.Error(fmt.Sprintf("MQTT connection lost: %s", err.Error()))
os.Exit(1)
})
client := mqtt.NewClient(opts)
if token := client.Connect(); token.Wait() && token.Error() != nil {
logger.Error(fmt.Sprintf("Failed to connect to Lora MQTT broker: %s", token.Error()))
os.Exit(1)
}
return client
}
func connectToRedis(redisURL, redisPass, redisDB string, logger logger.Logger) *r.Client {
db, err := strconv.Atoi(redisDB)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to redis: %s", err))
os.Exit(1)
}
return r.NewClient(&r.Options{
Addr: redisURL,
Password: redisPass,
DB: db,
})
}
func subscribeToLoRaBroker(svc lora.Service, mc mqtt.Client, logger logger.Logger) {
mqttBroker := mqttBroker.NewBroker(svc, mc, logger)
logger.Info("Subscribed to Lora MQTT broker")
if err := mqttBroker.Subscribe(loraServerTopic); err != nil {
logger.Error(fmt.Sprintf("Failed to subscribe to Lora MQTT broker: %s", err))
os.Exit(1)
}
}
func subscribeToThingsES(svc lora.Service, client *r.Client, consumer string, logger logger.Logger) {
eventStore := redis.NewEventStore(svc, client, consumer, logger)
logger.Info("Subscribed to Redis Event Store")
if err := eventStore.Subscribe("mainflux.things"); err != nil {
logger.Warn(fmt.Sprintf("Lora-adapter service failed to subscribe to event sourcing: %s", err))
}
}
func newRouteMapRepositoy(client *r.Client, prefix string, logger logger.Logger) lora.RouteMapRepository {
logger.Info("Connected to Redis Route map")
return redis.NewRouteMapRepository(client, prefix)
}
func startHTTPServer(cfg config, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", cfg.httpPort)
logger.Info(fmt.Sprintf("Lora-adapter service started, exposed port %s", cfg.httpPort))
errs <- http.ListenAndServe(p, api.MakeHandler())
}
-108
View File
@@ -1,108 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/manager"
"github.com/mainflux/mainflux/manager/api"
"github.com/mainflux/mainflux/manager/bcrypt"
"github.com/mainflux/mainflux/manager/jwt"
"github.com/mainflux/mainflux/manager/postgres"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
defDBHost string = "localhost"
defDBPort string = "5432"
defDBUser string = "mainflux"
defDBPass string = "mainflux"
defDBName string = "manager"
defPort string = "8180"
defSecret string = "manager"
envDBHost string = "MF_DB_HOST"
envDBPort string = "MF_DB_PORT"
envDBUser string = "MF_DB_USER"
envDBPass string = "MF_DB_PASS"
envDBName string = "MF_MANAGER_DB"
envPort string = "MF_MANAGER_PORT"
envSecret string = "MF_MANAGER_SECRET"
)
type config struct {
DBHost string
DBPort string
DBUser string
DBPass string
DBName string
Port string
Secret string
}
func main() {
cfg := config{
DBHost: mainflux.Env(envDBHost, defDBHost),
DBPort: mainflux.Env(envDBPort, defDBPort),
DBUser: mainflux.Env(envDBUser, defDBUser),
DBPass: mainflux.Env(envDBPass, defDBPass),
DBName: mainflux.Env(envDBName, defDBName),
Port: mainflux.Env(envPort, defPort),
Secret: mainflux.Env(envSecret, defSecret),
}
logger := log.New(os.Stdout)
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
os.Exit(1)
}
defer db.Close()
users := postgres.NewUserRepository(db)
clients := postgres.NewClientRepository(db)
channels := postgres.NewChannelRepository(db)
hasher := bcrypt.New()
idp := jwt.New(cfg.Secret)
svc := manager.New(users, clients, channels, hasher, idp)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "manager",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "manager",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%s", cfg.Port)
logger.Info(fmt.Sprintf("Manager service started, exposed port %s", cfg.Port))
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("Manager service terminated: %s", err))
}
+171
View File
@@ -0,0 +1,171 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/readers"
"github.com/mainflux/mainflux/readers/api"
"github.com/mainflux/mainflux/readers/mongodb"
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
"github.com/mongodb/mongo-go-driver/mongo"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
defThingsURL = "localhost:8181"
defLogLevel = "error"
defPort = "8180"
defDBName = "mainflux"
defDBHost = "localhost"
defDBPort = "27017"
defClientTLS = "false"
defCACerts = ""
envThingsURL = "MF_THINGS_URL"
envLogLevel = "MF_MONGO_READER_LOG_LEVEL"
envPort = "MF_MONGO_READER_PORT"
envDBName = "MF_MONGO_READER_DB_NAME"
envDBHost = "MF_MONGO_READER_DB_HOST"
envDBPort = "MF_MONGO_READER_DB_PORT"
envClientTLS = "MF_MONGO_READER_CLIENT_TLS"
envCACerts = "MF_MONGO_READER_CA_CERTS"
)
type config struct {
thingsURL string
logLevel string
port string
dbName string
dbHost string
dbPort string
clientTLS bool
caCerts string
}
func main() {
cfg := loadConfigs()
logger, err := logger.New(os.Stdout, cfg.logLevel)
if err != nil {
log.Fatalf(err.Error())
}
conn := connectToThings(cfg, logger)
defer conn.Close()
tc := thingsapi.NewClient(conn)
db := connectToMongoDB(cfg.dbHost, cfg.dbPort, cfg.dbName, logger)
repo := newService(db, logger)
errs := make(chan error, 2)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
go startHTTPServer(repo, tc, cfg.port, logger, errs)
err = <-errs
logger.Error(fmt.Sprintf("MongoDB reader service terminated: %s", err))
}
func loadConfigs() config {
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
if err != nil {
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
}
return config{
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
logLevel: mainflux.Env(envLogLevel, defLogLevel),
port: mainflux.Env(envPort, defPort),
dbName: mainflux.Env(envDBName, defDBName),
dbHost: mainflux.Env(envDBHost, defDBHost),
dbPort: mainflux.Env(envDBPort, defDBPort),
clientTLS: tls,
caCerts: mainflux.Env(envCACerts, defCACerts),
}
}
func connectToMongoDB(host, port, name string, logger logger.Logger) *mongo.Database {
client, err := mongo.Connect(context.Background(), fmt.Sprintf("mongodb://%s:%s", host, port), nil)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to database: %s", err))
os.Exit(1)
}
return client.Database(name)
}
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
var opts []grpc.DialOption
if cfg.clientTLS {
if cfg.caCerts != "" {
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
if err != nil {
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
os.Exit(1)
}
opts = append(opts, grpc.WithTransportCredentials(tpc))
}
} else {
logger.Info("gRPC communication is not encrypted")
opts = append(opts, grpc.WithInsecure())
}
conn, err := grpc.Dial(cfg.thingsURL, opts...)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
os.Exit(1)
}
return conn
}
func newService(db *mongo.Database, logger logger.Logger) readers.MessageRepository {
repo := mongodb.New(db)
repo = api.LoggingMiddleware(repo, logger)
repo = api.MetricsMiddleware(
repo,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "mongodb",
Subsystem: "message_reader",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "mongodb",
Subsystem: "message_reader",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
return repo
}
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, port string, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("Mongo reader service started, exposed port %s", port))
errs <- http.ListenAndServe(p, api.MakeHandler(repo, tc, "mongodb-reader"))
}
+132
View File
@@ -0,0 +1,132 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/writers"
"github.com/mainflux/mainflux/writers/mongodb"
"github.com/mongodb/mongo-go-driver/mongo"
"github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
queue = "mongodb-writer"
defNatsURL = nats.DefaultURL
defLogLevel = "error"
defPort = "8180"
defDBName = "mainflux"
defDBHost = "localhost"
defDBPort = "27017"
envNatsURL = "MF_NATS_URL"
envLogLevel = "MF_MONGO_WRITER_LOG_LEVEL"
envPort = "MF_MONGO_WRITER_PORT"
envDBName = "MF_MONGO_WRITER_DB_NAME"
envDBHost = "MF_MONGO_WRITER_DB_HOST"
envDBPort = "MF_MONGO_WRITER_DB_PORT"
)
type config struct {
NatsURL string
LogLevel string
Port string
DBName string
DBHost string
DBPort string
}
func main() {
cfg := loadConfigs()
logger, err := logger.New(os.Stdout, cfg.LogLevel)
if err != nil {
log.Fatalf(err.Error())
}
nc, err := nats.Connect(cfg.NatsURL)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
client, err := mongo.Connect(context.Background(), fmt.Sprintf("mongodb://%s:%s", cfg.DBHost, cfg.DBPort), nil)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to database: %s", err))
os.Exit(1)
}
db := client.Database(cfg.DBName)
repo := mongodb.New(db)
counter, latency := makeMetrics()
repo = writers.LoggingMiddleware(repo, logger)
repo = writers.MetricsMiddleware(repo, counter, latency)
if err := writers.Start(nc, repo, queue, logger); err != nil {
logger.Error(fmt.Sprintf("Failed to start message writer: %s", err))
os.Exit(1)
}
errs := make(chan error, 2)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
go startHTTPService(cfg.Port, logger, errs)
err = <-errs
logger.Error(fmt.Sprintf("MongoDB writer service terminated: %s", err))
}
func loadConfigs() config {
return config{
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
LogLevel: mainflux.Env(envLogLevel, defLogLevel),
Port: mainflux.Env(envPort, defPort),
DBName: mainflux.Env(envDBName, defDBName),
DBHost: mainflux.Env(envDBHost, defDBHost),
DBPort: mainflux.Env(envDBPort, defDBPort),
}
}
func makeMetrics() (*kitprometheus.Counter, *kitprometheus.Summary) {
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "mongodb",
Subsystem: "message_writer",
Name: "request_count",
Help: "Number of database inserts.",
}, []string{"method"})
latency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "mongodb",
Subsystem: "message_writer",
Name: "request_latency_microseconds",
Help: "Total duration of inserts in microseconds.",
}, []string{"method"})
return counter, latency
}
func startHTTPService(port string, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("Mongodb writer service started, exposed port %s", p))
errs <- http.ListenAndServe(p, mongodb.MakeHandler())
}
+55 -31
View File
@@ -1,54 +1,84 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/normalizer"
nats "github.com/nats-io/go-nats"
"github.com/mainflux/mainflux/normalizer/api"
"github.com/mainflux/mainflux/normalizer/nats"
broker "github.com/nats-io/go-nats"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)
const (
defNatsURL string = nats.DefaultURL
defPort string = "8180"
envNatsURL string = "MF_NATS_URL"
envPort string = "MF_NORMALIZER_PORT"
defNatsURL string = broker.DefaultURL
defLogLevel string = "error"
defPort string = "8180"
envNatsURL string = "MF_NATS_URL"
envLogLevel string = "MF_NORMALIZER_LOG_LEVEL"
envPort string = "MF_NORMALIZER_PORT"
)
type config struct {
NatsURL string
Port string
NatsURL string
LogLevel string
Port string
}
func main() {
cfg := config{
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
cfg := loadConfig()
logger, err := logger.New(os.Stdout, cfg.LogLevel)
if err != nil {
log.Fatalf(err.Error())
}
logger := log.New(os.Stdout)
nc, err := nats.Connect(cfg.NatsURL)
nc, err := broker.Connect(cfg.NatsURL)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
svc := normalizer.New()
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "normalizer",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "normalizer",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%s", cfg.Port)
logger.Info(fmt.Sprintf("Normalizer service started, exposed port %s", cfg.Port))
errs <- http.ListenAndServe(p, normalizer.MakeHandler())
errs <- http.ListenAndServe(p, api.MakeHandler())
}()
go func() {
@@ -57,22 +87,16 @@ func main() {
errs <- fmt.Errorf("%s", <-c)
}()
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "normalizer",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"})
latency := kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "normalizer",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"})
normalizer.Subscribe(nc, logger, counter, latency)
nats.Subscribe(svc, nc, logger)
err = <-errs
logger.Error(fmt.Sprintf("Normalizer service terminated: %s", err))
}
func loadConfig() config {
return config{
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
LogLevel: mainflux.Env(envLogLevel, defLogLevel),
Port: mainflux.Env(envPort, defPort),
}
}
+290
View File
@@ -0,0 +1,290 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"database/sql"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"google.golang.org/grpc/credentials"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/go-redis/redis"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/things"
"github.com/mainflux/mainflux/things/api"
grpcapi "github.com/mainflux/mainflux/things/api/grpc"
httpapi "github.com/mainflux/mainflux/things/api/http"
"github.com/mainflux/mainflux/things/postgres"
rediscache "github.com/mainflux/mainflux/things/redis"
"github.com/mainflux/mainflux/things/uuid"
usersapi "github.com/mainflux/mainflux/users/api/grpc"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
)
const (
defLogLevel = "error"
defDBHost = "localhost"
defDBPort = "5432"
defDBUser = "mainflux"
defDBPass = "mainflux"
defDBName = "things"
defDBSSLMode = "disable"
defDBSSLCert = ""
defDBSSLKey = ""
defDBSSLRootCert = ""
defClientTLS = "false"
defCACerts = ""
defCacheURL = "localhost:6379"
defCachePass = ""
defCacheDB = "0"
defESURL = "localhost:6379"
defESPass = ""
defESDB = "0"
defHTTPPort = "8180"
defGRPCPort = "8181"
defServerCert = ""
defServerKey = ""
defUsersURL = "localhost:8181"
envLogLevel = "MF_THINGS_LOG_LEVEL"
envDBHost = "MF_THINGS_DB_HOST"
envDBPort = "MF_THINGS_DB_PORT"
envDBUser = "MF_THINGS_DB_USER"
envDBPass = "MF_THINGS_DB_PASS"
envDBName = "MF_THINGS_DB"
envDBSSLMode = "MF_THINGS_DB_SSL_MODE"
envDBSSLCert = "MF_THINGS_DB_SSL_CERT"
envDBSSLKey = "MF_THINGS_DB_SSL_KEY"
envDBSSLRootCert = "MF_THINGS_DB_SSL_ROOT_CERT"
envClientTLS = "MF_THINGS_CLIENT_TLS"
envCACerts = "MF_THINGS_CA_CERTS"
envCacheURL = "MF_THINGS_CACHE_URL"
envCachePass = "MF_THINGS_CACHE_PASS"
envCacheDB = "MF_THINGS_CACHE_DB"
envESURL = "MF_THINGS_ES_URL"
envESPass = "MF_THINGS_ES_PASS"
envESDB = "MF_THINGS_ES_DB"
envHTTPPort = "MF_THINGS_HTTP_PORT"
envGRPCPort = "MF_THINGS_GRPC_PORT"
envUsersURL = "MF_USERS_URL"
envServerCert = "MF_THINGS_SERVER_CERT"
envServerKey = "MF_THINGS_SERVER_KEY"
)
type config struct {
logLevel string
dbConfig postgres.Config
clientTLS bool
caCerts string
cacheURL string
cachePass string
cacheDB string
esURL string
esPass string
esDB string
httpPort string
grpcPort string
usersURL string
serverCert string
serverKey string
}
func main() {
cfg := loadConfig()
logger, err := logger.New(os.Stdout, cfg.logLevel)
if err != nil {
log.Fatalf(err.Error())
}
cacheClient := connectToRedis(cfg.cacheURL, cfg.cachePass, cfg.cacheDB, logger)
esClient := connectToRedis(cfg.esURL, cfg.esPass, cfg.esDB, logger)
db := connectToDB(cfg.dbConfig, logger)
defer db.Close()
conn := connectToUsers(cfg, logger)
defer conn.Close()
svc := newService(conn, db, cacheClient, esClient, logger)
errs := make(chan error, 2)
go startHTTPServer(svc, cfg, logger, errs)
go startGRPCServer(svc, cfg, logger, errs)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("Things service terminated: %s", err))
}
func loadConfig() config {
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
if err != nil {
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
}
dbConfig := postgres.Config{
Host: mainflux.Env(envDBHost, defDBHost),
Port: mainflux.Env(envDBPort, defDBPort),
User: mainflux.Env(envDBUser, defDBUser),
Pass: mainflux.Env(envDBPass, defDBPass),
Name: mainflux.Env(envDBName, defDBName),
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
}
return config{
logLevel: mainflux.Env(envLogLevel, defLogLevel),
dbConfig: dbConfig,
clientTLS: tls,
caCerts: mainflux.Env(envCACerts, defCACerts),
cacheURL: mainflux.Env(envCacheURL, defCacheURL),
cachePass: mainflux.Env(envCachePass, defCachePass),
cacheDB: mainflux.Env(envCacheDB, defCacheDB),
esURL: mainflux.Env(envESURL, defESURL),
esPass: mainflux.Env(envESPass, defESPass),
esDB: mainflux.Env(envESDB, defESDB),
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
grpcPort: mainflux.Env(envGRPCPort, defGRPCPort),
usersURL: mainflux.Env(envUsersURL, defUsersURL),
serverCert: mainflux.Env(envServerCert, defServerCert),
serverKey: mainflux.Env(envServerKey, defServerKey),
}
}
func connectToRedis(cacheURL, cachePass string, cacheDB string, logger logger.Logger) *redis.Client {
db, err := strconv.Atoi(cacheDB)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to cache: %s", err))
os.Exit(1)
}
return redis.NewClient(&redis.Options{
Addr: cacheURL,
Password: cachePass,
DB: db,
})
}
func connectToDB(dbConfig postgres.Config, logger logger.Logger) *sql.DB {
db, err := postgres.Connect(dbConfig)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
os.Exit(1)
}
return db
}
func connectToUsers(cfg config, logger logger.Logger) *grpc.ClientConn {
var opts []grpc.DialOption
if cfg.clientTLS {
if cfg.caCerts != "" {
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
if err != nil {
logger.Error(fmt.Sprintf("Failed to create tls credentials: %s", err))
os.Exit(1)
}
opts = append(opts, grpc.WithTransportCredentials(tpc))
}
} else {
opts = append(opts, grpc.WithInsecure())
logger.Info("gRPC communication is not encrypted")
}
conn, err := grpc.Dial(cfg.usersURL, opts...)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
os.Exit(1)
}
return conn
}
func newService(conn *grpc.ClientConn, db *sql.DB, cacheClient *redis.Client, esClient *redis.Client, logger logger.Logger) things.Service {
users := usersapi.NewClient(conn)
thingsRepo := postgres.NewThingRepository(db, logger)
channelsRepo := postgres.NewChannelRepository(db, logger)
chanCache := rediscache.NewChannelCache(cacheClient)
thingCache := rediscache.NewThingCache(cacheClient)
idp := uuid.New()
svc := things.New(users, thingsRepo, channelsRepo, chanCache, thingCache, idp)
svc = rediscache.NewEventStoreMiddleware(svc, esClient)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "things",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "things",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
return svc
}
func startHTTPServer(svc things.Service, cfg config, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", cfg.httpPort)
if cfg.serverCert != "" || cfg.serverKey != "" {
logger.Info(fmt.Sprintf("Things service started using https on port %s with cert %s key %s",
cfg.httpPort, cfg.serverCert, cfg.serverKey))
errs <- http.ListenAndServeTLS(p, cfg.serverCert, cfg.serverKey, httpapi.MakeHandler(svc))
return
}
logger.Info(fmt.Sprintf("Things service started using http on port %s", cfg.httpPort))
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc))
}
func startGRPCServer(svc things.Service, cfg config, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", cfg.grpcPort)
listener, err := net.Listen("tcp", p)
if err != nil {
logger.Error(fmt.Sprintf("Failed to listen on port %s: %s", cfg.grpcPort, err))
os.Exit(1)
}
var server *grpc.Server
if cfg.serverCert != "" || cfg.serverKey != "" {
creds, err := credentials.NewServerTLSFromFile(cfg.serverCert, cfg.serverKey)
if err != nil {
logger.Error(fmt.Sprintf("Failed to load things certificates: %s", err))
os.Exit(1)
}
logger.Info(fmt.Sprintf("Things gRPC service started using https on port %s with cert %s key %s",
cfg.grpcPort, cfg.serverCert, cfg.serverKey))
server = grpc.NewServer(grpc.Creds(creds))
} else {
logger.Info(fmt.Sprintf("Things gRPC service started using http on port %s", cfg.grpcPort))
server = grpc.NewServer()
}
mainflux.RegisterThingsServiceServer(server, grpcapi.NewServer(svc))
errs <- server.Serve(listener)
}
+199
View File
@@ -0,0 +1,199 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"database/sql"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"google.golang.org/grpc/credentials"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/users"
"github.com/mainflux/mainflux/users/api"
grpcapi "github.com/mainflux/mainflux/users/api/grpc"
httpapi "github.com/mainflux/mainflux/users/api/http"
"github.com/mainflux/mainflux/users/bcrypt"
"github.com/mainflux/mainflux/users/jwt"
"github.com/mainflux/mainflux/users/postgres"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
)
const (
defLogLevel = "error"
defDBHost = "localhost"
defDBPort = "5432"
defDBUser = "mainflux"
defDBPass = "mainflux"
defDBName = "users"
defDBSSLMode = "disable"
defDBSSLCert = ""
defDBSSLKey = ""
defDBSSLRootCert = ""
defHTTPPort = "8180"
defGRPCPort = "8181"
defSecret = "users"
defServerCert = ""
defServerKey = ""
envLogLevel = "MF_USERS_LOG_LEVEL"
envDBHost = "MF_USERS_DB_HOST"
envDBPort = "MF_USERS_DB_PORT"
envDBUser = "MF_USERS_DB_USER"
envDBPass = "MF_USERS_DB_PASS"
envDBName = "MF_USERS_DB"
envDBSSLMode = "MF_USERS_DB_SSL_MODE"
envDBSSLCert = "MF_USERS_DB_SSL_CERT"
envDBSSLKey = "MF_USERS_DB_SSL_KEY"
envDBSSLRootCert = "MF_USERS_DB_SSL_ROOT_CERT"
envHTTPPort = "MF_USERS_HTTP_PORT"
envGRPCPort = "MF_USERS_GRPC_PORT"
envSecret = "MF_USERS_SECRET"
envServerCert = "MF_USERS_SERVER_CERT"
envServerKey = "MF_USERS_SERVER_KEY"
)
type config struct {
logLevel string
dbConfig postgres.Config
httpPort string
grpcPort string
secret string
serverCert string
serverKey string
}
func main() {
cfg := loadConfig()
logger, err := logger.New(os.Stdout, cfg.logLevel)
if err != nil {
log.Fatalf(err.Error())
}
db := connectToDB(cfg.dbConfig, logger)
defer db.Close()
svc := newService(db, cfg.secret, logger)
errs := make(chan error, 2)
go startHTTPServer(svc, cfg.httpPort, cfg.serverCert, cfg.serverKey, logger, errs)
go startGRPCServer(svc, cfg.grpcPort, cfg.serverCert, cfg.serverKey, logger, errs)
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("Users service terminated: %s", err))
}
func loadConfig() config {
dbConfig := postgres.Config{
Host: mainflux.Env(envDBHost, defDBHost),
Port: mainflux.Env(envDBPort, defDBPort),
User: mainflux.Env(envDBUser, defDBUser),
Pass: mainflux.Env(envDBPass, defDBPass),
Name: mainflux.Env(envDBName, defDBName),
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
}
return config{
logLevel: mainflux.Env(envLogLevel, defLogLevel),
dbConfig: dbConfig,
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
grpcPort: mainflux.Env(envGRPCPort, defGRPCPort),
secret: mainflux.Env(envSecret, defSecret),
serverCert: mainflux.Env(envServerCert, defServerCert),
serverKey: mainflux.Env(envServerKey, defServerKey),
}
}
func connectToDB(dbConfig postgres.Config, logger logger.Logger) *sql.DB {
db, err := postgres.Connect(dbConfig)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
os.Exit(1)
}
return db
}
func newService(db *sql.DB, secret string, logger logger.Logger) users.Service {
repo := postgres.New(db)
hasher := bcrypt.New()
idp := jwt.New(secret)
svc := users.New(repo, hasher, idp)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "users",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "users",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
return svc
}
func startHTTPServer(svc users.Service, port string, certFile string, keyFile string, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
if certFile != "" || keyFile != "" {
logger.Info(fmt.Sprintf("Users service started using https, cert %s key %s, exposed port %s", certFile, keyFile, port))
errs <- http.ListenAndServeTLS(p, certFile, keyFile, httpapi.MakeHandler(svc, logger))
} else {
logger.Info(fmt.Sprintf("Users service started using http, exposed port %s", port))
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc, logger))
}
}
func startGRPCServer(svc users.Service, port string, certFile string, keyFile string, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
listener, err := net.Listen("tcp", p)
if err != nil {
logger.Error(fmt.Sprintf("Failed to listen on port %s: %s", port, err))
}
var server *grpc.Server
if certFile != "" || keyFile != "" {
creds, err := credentials.NewServerTLSFromFile(certFile, keyFile)
if err != nil {
logger.Error(fmt.Sprintf("Failed to load users certificates: %s", err))
os.Exit(1)
}
logger.Info(fmt.Sprintf("Users gRPC service started using https on port %s with cert %s key %s", port, certFile, keyFile))
server = grpc.NewServer(grpc.Creds(creds))
} else {
logger.Info(fmt.Sprintf("Users gRPC service started using http on port %s", port))
server = grpc.NewServer()
}
mainflux.RegisterUsersServiceServer(server, grpcapi.NewServer(svc))
logger.Info(fmt.Sprintf("Users gRPC service started, exposed port %s", port))
errs <- server.Serve(listener)
}
+103 -35
View File
@@ -1,55 +1,139 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
manager "github.com/mainflux/mainflux/manager/client"
"github.com/mainflux/mainflux/logger"
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
adapter "github.com/mainflux/mainflux/ws"
"github.com/mainflux/mainflux/ws/api"
"github.com/mainflux/mainflux/ws/nats"
broker "github.com/nats-io/go-nats"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
defPort = "8180"
defNatsURL = broker.DefaultURL
defManagerURL = "http://localhost:8180"
envPort = "MF_WS_ADAPTER_PORT"
envNatsURL = "MF_NATS_URL"
envManagerURL = "MF_MANAGER_URL"
defClientTLS = "false"
defCACerts = ""
defPort = "8180"
defLogLevel = "error"
defNatsURL = broker.DefaultURL
defThingsURL = "localhost:8181"
envClientTLS = "MF_WS_ADAPTER_CLIENT_TLS"
envCACerts = "MF_WS_ADAPTER_CA_CERTS"
envPort = "MF_WS_ADAPTER_PORT"
envLogLevel = "MF_WS_ADAPTER_LOG_LEVEL"
envNatsURL = "MF_NATS_URL"
envThingsURL = "MF_THINGS_URL"
)
type config struct {
ManagerURL string
NatsURL string
Port string
clientTLS bool
caCerts string
thingsURL string
natsURL string
logLevel string
port string
}
func main() {
cfg := config{
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
cfg := loadConfig()
logger, err := logger.New(os.Stdout, cfg.logLevel)
if err != nil {
log.Fatalf(err.Error())
}
logger := log.New(os.Stdout)
nc, err := broker.Connect(cfg.NatsURL)
nc, err := broker.Connect(cfg.natsURL)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
os.Exit(1)
}
defer nc.Close()
conn := connectToThings(cfg, logger)
defer conn.Close()
cc := thingsapi.NewClient(conn)
pubsub := nats.New(nc)
svc := newService(pubsub, logger)
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%s", cfg.port)
logger.Info(fmt.Sprintf("WebSocket adapter service started, exposed port %s", cfg.port))
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc, logger))
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("WebSocket adapter terminated: %s", err))
}
func loadConfig() config {
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
if err != nil {
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
}
return config{
clientTLS: tls,
caCerts: mainflux.Env(envCACerts, defCACerts),
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
natsURL: mainflux.Env(envNatsURL, defNatsURL),
logLevel: mainflux.Env(envLogLevel, defLogLevel),
port: mainflux.Env(envPort, defPort),
}
}
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
var opts []grpc.DialOption
if cfg.clientTLS {
if cfg.caCerts != "" {
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
if err != nil {
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
os.Exit(1)
}
opts = append(opts, grpc.WithTransportCredentials(tpc))
}
} else {
logger.Info("gRPC communication is not encrypted")
opts = append(opts, grpc.WithInsecure())
}
conn, err := grpc.Dial(cfg.thingsURL, opts...)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
os.Exit(1)
}
return conn
}
func newService(pubsub adapter.Service, logger logger.Logger) adapter.Service {
svc := adapter.New(pubsub)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
@@ -68,21 +152,5 @@ func main() {
}, []string{"method"}),
)
errs := make(chan error, 2)
go func() {
p := fmt.Sprintf(":%s", cfg.Port)
mc := manager.NewClient(cfg.ManagerURL)
logger.Info(fmt.Sprintf("WebSocket adapter service started, exposed port %s", cfg.Port))
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc, logger))
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("WebSocket adapter terminated: %s", err))
return svc
}
+28 -13
View File
@@ -9,9 +9,15 @@ The service is configured using the environment variables presented in the
following table. Note that any unset variables will be replaced with their
default values.
| Variable | Description | Default |
|-----------------------|-------------------|-----------------------|
| COAP_ADAPTER_NATS_URL | NATS instance URL | nats://localhost:4222 |
| Variable | Description | Default |
|-----------------------------|--------------------------------------------------------|-----------------------|
| MF_COAP_ADAPTER_PORT | Service listening port | 5683 |
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
| MF_THINGS_URL | Things service URL | localhost:8181 |
| MF_COAP_ADAPTER_LOG_LEVEL | Service log level | error |
| MF_COAP_ADAPTER_CLIENT_TLS | Flag that indicates if TLS should be turned on | false |
| MF_COAP_ADAPTER_CA_CERTS | Path to trusted CAs in PEM format | |
| MF_COAP_ADAPTER_PING_PERIOD | Hours between 1 and 24 to ping client with ACK message | 12 |
## Deployment
@@ -22,31 +28,40 @@ a compose file template that can be used to deploy the service container locally
version: "2"
services:
adapter:
image: mainflux/coap-adapter:[version]
image: mainflux/coap:[version]
container_name: [instance name]
ports:
- [host machine port]:5683
- [host machine port]:[configured port]
environment:
COAP_ADAPTER_NATS_URL: [NATS instance URL]
MF_COAP_ADAPTER_PORT: [Service HTTP port]
MF_NATS_URL: [NATS instance URL]
MF_THINGS_URL: [Things service URL]
MF_COAP_ADAPTER_LOG_LEVEL: [Service log level]
MF_COAP_ADAPTER_CLIENT_TLS: [Flag that indicates if TLS should be turned on]
MF_COAP_ADAPTER_CA_CERTS: [Path to trusted CAs in PEM format]
MF_COAP_ADAPTER_PING_PERIOD: [Hours between 1 and 24 to ping client with ACK message]
```
Running this service outside of container requires working instance of the NATS service.
To start the service outside of the container, execute the following shell script:
```bash
# download the latest version of the service
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux/cmd/coap
cd $GOPATH/src/github.com/mainflux/mainflux
# compile the app; make sure to set the proper GOOS value
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
# compile the http
make coap
# copy binary to bin
make install
# set the environment variables and run the service
COAP_ADAPTER_NATS_URL=[NATS instance URL] app
MF_THINGS_URL=[Things service URL] MF_NATS_URL=[NATS instance URL] MF_COAP_ADAPTER_PORT=[Service HTTP port] MF_COAP_ADAPTER_LOG_LEVEL=[Service log level] MF_COAP_ADAPTER_CLIENT_TLS=[Flag that indicates if TLS should be turned on] MF_COAP_ADAPTER_CA_CERTS=[Path to trusted CAs in PEM format] MF_COAP_ADAPTER_PING_PERIOD: [Hours between 1 and 24 to ping client with ACK message] $GOBIN/mainflux-coap
```
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
If CoAP adapter is running locally (on default 5683 port), a valid URL would be: `coap://localhost/channels/<channel_id>/messages?authorization=<thing_auth_key>`.
Since CoAP protocol does not support `Authorization` header (option) and options have limited size, in order to send CoAP messages, valid `authorization` value must be present in `Uri-Query` option.
+127 -38
View File
@@ -1,61 +1,150 @@
package adapter
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
// Package coap contains the domain concept definitions needed to support
// Mainflux coap adapter service functionality. All constant values are taken
// from RFC, and could be adjusted based on specific use case.
package coap
import (
"encoding/json"
"log"
"net"
"errors"
"sync"
"time"
"github.com/dustin/go-coap"
"github.com/mainflux/mainflux"
broker "github.com/nats-io/go-nats"
"go.uber.org/zap"
)
const protocol string = "coap"
const (
chanID = "id"
keyHeader = "key"
type Observer struct {
conn *net.UDPConn
addr *net.UDPAddr
message *coap.Message
// AckRandomFactor is default ACK coefficient.
AckRandomFactor = 1.5
// AckTimeout is the amount of time to wait for a response.
AckTimeout = 2000 * time.Millisecond
// MaxRetransmit is the maximum number of times a message will be retransmitted.
MaxRetransmit = 4
)
var (
errBadOption = errors.New("bad option")
// ErrFailedMessagePublish indicates that message publishing failed.
ErrFailedMessagePublish = errors.New("failed to publish message")
// ErrFailedSubscription indicates that client couldn't subscribe to specified channel.
ErrFailedSubscription = errors.New("failed to subscribe to a channel")
// ErrFailedConnection indicates that service couldn't connect to message broker.
ErrFailedConnection = errors.New("failed to connect to message broker")
)
// Broker represents NATS broker instance.
type Broker interface {
mainflux.MessagePublisher
// Subscribes to channel with specified id, subtopic and adds subscription to
// service map of subscriptions under given ID.
Subscribe(string, string, string, *Observer) error
}
type CoAPAdapter struct {
obsMap map[string][]Observer
logger *zap.Logger
pub mainflux.MessagePublisher
// Service specifies coap service API.
type Service interface {
Broker
// Unsubscribe method is used to stop observing resource.
Unsubscribe(string)
}
// NewCoAPAdapter creates new CoAP adapter struct
func NewCoAPAdapter(logger *zap.Logger, pub mainflux.MessagePublisher) *CoAPAdapter {
ca := &CoAPAdapter{
logger: logger,
pub: pub,
obsMap: make(map[string][]Observer),
var _ Service = (*adapterService)(nil)
type adapterService struct {
pubsub Broker
obs map[string]*Observer
obsLock sync.Mutex
}
// New instantiates the CoAP adapter implementation.
func New(pubsub Broker, responses <-chan string) Service {
as := &adapterService{
pubsub: pubsub,
obs: make(map[string]*Observer),
obsLock: sync.Mutex{},
}
return ca
go as.listenResponses(responses)
return as
}
// Serve function starts CoAP server
func (ca *CoAPAdapter) Serve(addr string) error {
ca.logger.Info("Starting CoAP server", zap.String("address", addr))
return coap.ListenAndServe("udp", addr, ca.COAPServer())
func (svc *adapterService) get(obsID string) (*Observer, bool) {
svc.obsLock.Lock()
defer svc.obsLock.Unlock()
val, ok := svc.obs[obsID]
return val, ok
}
// BridgeHandler functions is a handler for messages received via NATS
func (ca *CoAPAdapter) BridgeHandler(nm *broker.Msg) {
log.Printf("Received a message: %s\n", string(nm.Data))
func (svc *adapterService) put(obsID string, o *Observer) {
svc.obsLock.Lock()
defer svc.obsLock.Unlock()
// And write it into the database
m := mainflux.RawMessage{}
if len(nm.Data) > 0 {
if err := json.Unmarshal(nm.Data, &m); err != nil {
log.Println("Can not decode adapter msg")
return
val, ok := svc.obs[obsID]
if ok {
close(val.Cancel)
}
svc.obs[obsID] = o
}
func (svc *adapterService) remove(obsID string) {
svc.obsLock.Lock()
defer svc.obsLock.Unlock()
val, ok := svc.obs[obsID]
if ok {
close(val.Cancel)
delete(svc.obs, obsID)
}
}
// ListenResponses method handles ACK messages received from client.
func (svc *adapterService) listenResponses(responses <-chan string) {
for {
id := <-responses
val, ok := svc.get(id)
if ok {
val.StoreExpired(false)
}
}
}
func (svc *adapterService) Publish(msg mainflux.RawMessage) error {
if err := svc.pubsub.Publish(msg); err != nil {
switch err {
case broker.ErrConnectionClosed, broker.ErrInvalidConnection:
return ErrFailedConnection
default:
return ErrFailedMessagePublish
}
}
log.Println("Calling obsTransmit()")
log.Println(m.Publisher, m.Protocol, m.Channel, m.Payload)
ca.obsTransmit(m)
return nil
}
func (svc *adapterService) Subscribe(chanID, subtopic, obsID string, o *Observer) error {
if err := svc.pubsub.Subscribe(chanID, subtopic, obsID, o); err != nil {
return ErrFailedSubscription
}
// Put method removes Observer if already exists.
svc.put(obsID, o)
return nil
}
func (svc *adapterService) Unsubscribe(obsID string) {
svc.remove(obsID)
}
+7
View File
@@ -1,3 +1,10 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
// Package api contains API-related concerns: endpoint definitions, middlewares
// and all resource representations.
package api
+74
View File
@@ -0,0 +1,74 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
// +build !test
package api
import (
"fmt"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap"
log "github.com/mainflux/mainflux/logger"
)
var _ coap.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger log.Logger
svc coap.Service
}
// LoggingMiddleware adds logging facilities to the adapter.
func LoggingMiddleware(svc coap.Service, logger log.Logger) coap.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) (err error) {
defer func(begin time.Time) {
destChannel := msg.Channel
if msg.Subtopic != "" {
destChannel = fmt.Sprintf("%s.%s", destChannel, msg.Subtopic)
}
message := fmt.Sprintf("Method publish to channel %s took %s to complete", destChannel, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Publish(msg)
}
func (lm *loggingMiddleware) Subscribe(chanID, subtopic, obsID string, o *coap.Observer) (err error) {
defer func(begin time.Time) {
destChannel := chanID
if subtopic != "" {
destChannel = fmt.Sprintf("%s.%s", destChannel, subtopic)
}
message := fmt.Sprintf("Method subscribe to channel %s for client %s took %s to complete", destChannel, obsID, time.Since(begin))
if err != nil {
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
return
}
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())
return lm.svc.Subscribe(chanID, subtopic, obsID, o)
}
func (lm *loggingMiddleware) Unsubscribe(obsID string) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method unsubscribe for the client %s took %s to complete without errors.", obsID, time.Since(begin))
lm.logger.Info(fmt.Sprintf(message))
}(time.Now())
lm.svc.Unsubscribe(obsID)
}
+62
View File
@@ -0,0 +1,62 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
// +build !test
package api
import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap"
)
var _ coap.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc coap.Service
}
// MetricsMiddleware instruments adapter by tracking request count and latency.
func MetricsMiddleware(svc coap.Service, counter metrics.Counter, latency metrics.Histogram) coap.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (mm *metricsMiddleware) Publish(msg mainflux.RawMessage) error {
defer func(begin time.Time) {
mm.counter.With("method", "publish").Add(1)
mm.latency.With("method", "publish").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Publish(msg)
}
func (mm *metricsMiddleware) Subscribe(chanID, subtopic, clientID string, o *coap.Observer) error {
defer func(begin time.Time) {
mm.counter.With("method", "subscribe").Add(1)
mm.latency.With("method", "subscribe").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.Subscribe(chanID, subtopic, clientID, o)
}
func (mm *metricsMiddleware) Unsubscribe(clientID string) {
defer func(begin time.Time) {
mm.counter.With("method", "unsubscribe").Add(1)
mm.latency.With("method", "unsubscribe").Observe(time.Since(begin).Seconds())
}(time.Now())
mm.svc.Unsubscribe(clientID)
}
+339
View File
@@ -0,0 +1,339 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package api
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux/coap"
log "github.com/mainflux/mainflux/logger"
"github.com/prometheus/client_golang/prometheus/promhttp"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
mux "github.com/dereulenspiegel/coap-mux"
gocoap "github.com/dustin/go-coap"
"github.com/mainflux/mainflux"
)
const protocol = "coap"
var (
errBadRequest = errors.New("bad request")
errBadOption = errors.New("bad option")
errMalformedSubtopic = errors.New("malformed subtopic")
)
var (
auth mainflux.ThingsServiceClient
logger log.Logger
pingPeriod time.Duration
)
type handler func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message
func notFoundHandler(l *net.UDPConn, a *net.UDPAddr, m *gocoap.Message) *gocoap.Message {
if m.IsConfirmable() {
return &gocoap.Message{
Type: gocoap.Acknowledgement,
Code: gocoap.NotFound,
}
}
return nil
}
//MakeHTTPHandler creates handler for version endpoint.
func MakeHTTPHandler() http.Handler {
b := bone.New()
b.GetFunc("/version", mainflux.Version(protocol))
b.Handle("/metrics", promhttp.Handler())
return b
}
// MakeCOAPHandler creates handler for CoAP messages.
func MakeCOAPHandler(svc coap.Service, tc mainflux.ThingsServiceClient, l log.Logger, responses chan<- string, pp time.Duration) gocoap.Handler {
auth = tc
logger = l
pingPeriod = pp
r := mux.NewRouter()
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(receive(svc))).Methods(gocoap.POST)
r.Handle("/channels/{id}/messages/{subtopic:[^?]*}", gocoap.FuncHandler(receive(svc))).Methods(gocoap.POST)
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(observe(svc, responses)))
r.Handle("/channels/{id}/messages/{subtopic:[^?]*}", gocoap.FuncHandler(observe(svc, responses)))
r.NotFoundHandler = gocoap.FuncHandler(notFoundHandler)
return r
}
func authorize(msg *gocoap.Message, res *gocoap.Message, cid string) (string, error) {
// Device Key is passed as Uri-Query parameter, which option ID is 15 (0xf).
key, err := authKey(msg.Option(gocoap.URIQuery))
if err != nil {
switch err {
case errBadOption:
res.Code = gocoap.BadOption
case errBadRequest:
res.Code = gocoap.BadRequest
}
return "", err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
id, err := auth.CanAccess(ctx, &mainflux.AccessReq{Token: key, ChanID: cid})
if err != nil {
e, ok := status.FromError(err)
if ok {
switch e.Code() {
case codes.PermissionDenied:
res.Code = gocoap.Forbidden
default:
res.Code = gocoap.ServiceUnavailable
}
return "", err
}
res.Code = gocoap.InternalServerError
}
return id.GetValue(), nil
}
func fmtSubtopic(subtopic string) (string, error) {
if subtopic == "" {
return subtopic, nil
}
subtopic = strings.Replace(subtopic, "/", ".", -1)
elems := strings.Split(subtopic, ".")
filteredElems := []string{}
for _, elem := range elems {
if elem == "" {
continue
}
if len(elem) > 1 && (strings.Contains(elem, "*") || strings.Contains(elem, ">")) {
return "", errMalformedSubtopic
}
filteredElems = append(filteredElems, elem)
}
subtopic = strings.Join(filteredElems, ".")
return subtopic, nil
}
func receive(svc coap.Service) handler {
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
// By default message is NonConfirmable, so
// NonConfirmable response is sent back.
res := &gocoap.Message{
Type: gocoap.NonConfirmable,
// According to https://tools.ietf.org/html/rfc7252#page-47: If the POST
// succeeds but does not result in a new resource being created on the
// server, the response SHOULD have a 2.04 (Changed) Response Code.
Code: gocoap.Changed,
MessageID: msg.MessageID,
Token: msg.Token,
Payload: []byte{},
}
if msg.IsConfirmable() {
res.Type = gocoap.Acknowledgement
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
if len(msg.Payload) == 0 {
res.Code = gocoap.BadRequest
return res
}
}
chanID := mux.Var(msg, "id")
if chanID == "" {
res.Code = gocoap.NotFound
return res
}
subtopic, err := fmtSubtopic(mux.Var(msg, "subtopic"))
if err != nil {
res.Code = gocoap.BadRequest
return res
}
publisher, err := authorize(msg, res, chanID)
if err != nil {
res.Code = gocoap.Forbidden
return res
}
rawMsg := mainflux.RawMessage{
Channel: chanID,
Subtopic: subtopic,
Publisher: publisher,
Protocol: protocol,
Payload: msg.Payload,
}
if err := svc.Publish(rawMsg); err != nil {
res.Code = gocoap.InternalServerError
}
return res
}
}
func observe(svc coap.Service, responses chan<- string) handler {
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
res := &gocoap.Message{
Type: gocoap.Acknowledgement,
Code: gocoap.Content,
MessageID: msg.MessageID,
Token: msg.Token,
Payload: []byte{},
}
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
chanID := mux.Var(msg, "id")
if chanID == "" {
res.Code = gocoap.NotFound
return res
}
subtopic, err := fmtSubtopic(mux.Var(msg, "subtopic"))
if err != nil {
res.Code = gocoap.BadRequest
return res
}
publisher, err := authorize(msg, res, chanID)
if err != nil {
res.Code = gocoap.Forbidden
logger.Warn(fmt.Sprintf("Failed to authorize: %s", err))
return res
}
obsID := fmt.Sprintf("%x-%s-%s", msg.Token, publisher, chanID)
if msg.Type == gocoap.Acknowledgement {
responses <- obsID
return nil
}
if value, ok := msg.Option(gocoap.Observe).(uint32); (ok && value == 1) || msg.Type == gocoap.Reset {
svc.Unsubscribe(obsID)
}
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 0 {
res.AddOption(gocoap.Observe, 1)
o := coap.NewObserver()
if err := svc.Subscribe(chanID, subtopic, obsID, o); err != nil {
logger.Warn(fmt.Sprintf("Failed to subscribe to NATS subject: %s", err))
res.Code = gocoap.InternalServerError
return res
}
go handleMessage(conn, addr, o, msg)
go ping(svc, obsID, conn, addr, o, msg)
go cancel(o)
}
return res
}
}
func cancel(observer *coap.Observer) {
<-observer.Cancel
close(observer.Messages)
observer.StoreExpired(true)
}
func handleMessage(conn *net.UDPConn, addr *net.UDPAddr, o *coap.Observer, msg *gocoap.Message) {
notifyMsg := *msg
notifyMsg.Type = gocoap.NonConfirmable
notifyMsg.Code = gocoap.Content
notifyMsg.RemoveOption(gocoap.URIQuery)
for {
msg, ok := <-o.Messages
if !ok {
return
}
payload, err := json.Marshal(msg)
if err != nil {
logger.Warn(fmt.Sprintf("Failed to parse received message: %s", err))
continue
}
notifyMsg.Payload = payload
notifyMsg.MessageID = o.LoadMessageID()
buff := new(bytes.Buffer)
observe := uint64(notifyMsg.MessageID)
if err := binary.Write(buff, binary.BigEndian, observe); err != nil {
logger.Warn(fmt.Sprintf("Failed to generate Observe option value: %s", err))
continue
}
observeVal := buff.Bytes()
notifyMsg.SetOption(gocoap.Observe, observeVal[len(observeVal)-3:])
if err := gocoap.Transmit(conn, addr, notifyMsg); err != nil {
logger.Warn(fmt.Sprintf("Failed to send message to observer: %s", err))
}
}
}
func ping(svc coap.Service, obsID string, conn *net.UDPConn, addr *net.UDPAddr, o *coap.Observer, msg *gocoap.Message) {
pingMsg := *msg
pingMsg.Payload = []byte{}
pingMsg.Type = gocoap.Confirmable
pingMsg.RemoveOption(gocoap.URIQuery)
// According to RFC (https://tools.ietf.org/html/rfc7641#page-18), CON message must be sent at least every
// 24 hours. Deafault value of pingPeriod is 12.
t := time.NewTicker(pingPeriod * time.Hour)
defer t.Stop()
for {
select {
case _, ok := <-t.C:
if !ok || o.LoadExpired() {
return
}
o.StoreExpired(true)
timeout := float64(coap.AckTimeout)
logger.Info(fmt.Sprintf("Ping client %s.", obsID))
for i := 0; i < coap.MaxRetransmit; i++ {
pingMsg.MessageID = o.LoadMessageID()
gocoap.Transmit(conn, addr, pingMsg)
time.Sleep(time.Duration(timeout * coap.AckRandomFactor))
if !o.LoadExpired() {
break
}
timeout = 2 * timeout
}
if o.LoadExpired() {
svc.Unsubscribe(obsID)
return
}
case <-o.Cancel:
return
}
}
}
+24
View File
@@ -0,0 +1,24 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package api
import "strings"
func authKey(opt interface{}) (string, error) {
val, ok := opt.(string)
if !ok {
return "", errBadRequest
}
arr := strings.Split(val, "=")
if len(arr) != 2 || strings.ToLower(arr[0]) != "authorization" {
return "", errBadOption
}
return arr[1], nil
}
-145
View File
@@ -1,145 +0,0 @@
package adapter
import (
"bytes"
"log"
"net"
mux "github.com/dereulenspiegel/coap-mux"
coap "github.com/dustin/go-coap"
"github.com/mainflux/mainflux"
)
func (ca *CoAPAdapter) sendMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
log.Printf("Got message in sendMessage: path=%q: %#v from %v", m.Path(), m, a)
var res *coap.Message
if m.IsConfirmable() {
res = &coap.Message{
Type: coap.Acknowledgement,
Code: coap.Content,
MessageID: m.MessageID,
Token: m.Token,
Payload: []byte(""),
}
res.SetOption(coap.ContentFormat, coap.AppJSON)
}
if len(m.Payload) == 0 {
if m.IsConfirmable() {
res.Code = coap.BadRequest
}
return res
}
// Channel ID
cid := mux.Var(m, "channel_id")
n := mainflux.RawMessage{
Channel: cid,
Publisher: "",
Protocol: protocol,
Payload: m.Payload,
}
if err := ca.pub.Publish(n); err != nil {
if m.IsConfirmable() {
res.Code = coap.InternalServerError
}
return res
}
if m.IsConfirmable() {
res.Code = coap.Changed
}
return res
}
func (ca *CoAPAdapter) registerObserver(o Observer, cid string) {
found := false
for _, v := range ca.obsMap[cid] {
if v.addr == o.addr && bytes.Compare(v.message.Token, o.message.Token) == 0 {
found = true
break
}
}
if !found {
log.Println("Register " + cid)
log.Printf("o.message = %v", o.message)
ca.obsMap[cid] = append(ca.obsMap[cid], o)
}
}
func (ca *CoAPAdapter) deregisterObserver(o Observer, cid string) {
for k, v := range ca.obsMap[cid] {
if bytes.Compare(v.message.Token, o.message.Token) == 0 {
// Observer found, remove it from array
log.Println("Deregister " + cid)
ca.obsMap[cid] = append((ca.obsMap[cid])[:k], (ca.obsMap[cid])[k+1:]...)
}
}
}
func (ca *CoAPAdapter) observeMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
log.Printf("Got message in observeMessage: path=%q: %#v from %v", m.Path(), m, a)
var res *coap.Message
if m.IsConfirmable() {
res = &coap.Message{
Type: coap.Acknowledgement,
Code: coap.Content,
MessageID: m.MessageID,
Token: m.Token,
Payload: []byte(""),
}
res.SetOption(coap.ContentFormat, coap.AppJSON)
}
// Channel ID
cid := mux.Var(m, "channel_id")
// Observer
o := Observer{
conn: l,
addr: a,
message: m,
}
if m.Option(coap.Observe) == nil {
if m.IsConfirmable() {
res.Code = coap.BadRequest
}
return res
}
if value, ok := m.Option(coap.Observe).(uint32); ok && value == 0 {
ca.registerObserver(o, cid)
} else {
ca.deregisterObserver(o, cid)
}
if m.IsConfirmable() {
res.Code = coap.Valid
}
return res
}
func (ca *CoAPAdapter) obsTransmit(n mainflux.RawMessage) {
for _, v := range ca.obsMap[n.Channel] {
msg := *(v.message)
msg.Payload = n.Payload
log.Printf("ca.obsMap[cid] = %v", v)
log.Printf("msg = %v", msg)
msg.SetOption(coap.ContentFormat, coap.AppJSON)
msg.SetOption(coap.LocationPath, msg.Path())
log.Printf("Transmitting %v", msg)
err := coap.Transmit(v.conn, v.addr, msg)
if err != nil {
log.Printf("Error on transmitter, stopping: %v", err)
return
}
}
}
+73
View File
@@ -0,0 +1,73 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
// Package nats contains NATS message publisher implementation.
package nats
import (
"fmt"
"github.com/gogo/protobuf/proto"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap"
broker "github.com/nats-io/go-nats"
)
const prefix = "channel"
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
type natsPublisher struct {
nc *broker.Conn
}
// New instantiates NATS message publisher.
func New(nc *broker.Conn) coap.Broker {
return &natsPublisher{nc}
}
func (pubsub *natsPublisher) fmtSubject(chanID, subtopic string) string {
subject := fmt.Sprintf("%s.%s", prefix, chanID)
if subtopic != "" {
subject = fmt.Sprintf("%s.%s", subject, subtopic)
}
return subject
}
func (pubsub *natsPublisher) Publish(msg mainflux.RawMessage) error {
data, err := proto.Marshal(&msg)
if err != nil {
return err
}
subject := pubsub.fmtSubject(msg.Channel, msg.Subtopic)
return pubsub.nc.Publish(subject, data)
}
func (pubsub *natsPublisher) Subscribe(chanID, subtopic, obsID string, observer *coap.Observer) error {
subject := pubsub.fmtSubject(chanID, subtopic)
sub, err := pubsub.nc.Subscribe(subject, func(msg *broker.Msg) {
if msg == nil {
return
}
var rawMsg mainflux.RawMessage
if err := proto.Unmarshal(msg.Data, &rawMsg); err != nil {
return
}
observer.Messages <- rawMsg
})
if err != nil {
return err
}
go func() {
<-observer.Cancel
sub.Unsubscribe()
}()
return nil
}
+72
View File
@@ -0,0 +1,72 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package coap
import (
"sync"
"github.com/mainflux/mainflux"
)
// Observer is used to handle CoAP subscription.
type Observer struct {
// Expired flag is used to mark that ticker sent a
// CON message, but response is not received yet.
// The flag changes its value once ACK message is
// received from the client. If Expired is true
// when ticker is triggered, Observer should be canceled
// and removed from the Service map.
expired bool
// Message ID for notification messages.
msgID uint16
expiredLock, msgIDLock sync.Mutex
// Messages is used to receive messages from NATS.
Messages chan mainflux.RawMessage
// Cancel channel is used to cancel observing resource.
// Cancel channel should not be used to send or receive any
// data, it's purpose is to be closed once Observer canceled.
Cancel chan bool
}
// NewObserver instantiates a new Observer.
func NewObserver() *Observer {
return &Observer{
Messages: make(chan mainflux.RawMessage),
Cancel: make(chan bool),
}
}
// LoadExpired reads Expired flag in thread-safe way.
func (o *Observer) LoadExpired() bool {
o.expiredLock.Lock()
defer o.expiredLock.Unlock()
return o.expired
}
// StoreExpired stores Expired flag in thread-safe way.
func (o *Observer) StoreExpired(val bool) {
o.expiredLock.Lock()
defer o.expiredLock.Unlock()
o.expired = val
}
// LoadMessageID reads MessageID and increments
// its value in thread-safe way.
func (o *Observer) LoadMessageID() uint16 {
o.msgIDLock.Lock()
defer o.msgIDLock.Unlock()
o.msgID++
return o.msgID
}
-30
View File
@@ -1,30 +0,0 @@
package adapter
import (
"log"
"net"
"github.com/dereulenspiegel/coap-mux"
"github.com/dustin/go-coap"
)
func notFoundHandler(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
log.Printf("Got message in notFoundHandler: path=%q: %#v from %v", m.Path(), m, a)
if m.IsConfirmable() {
return &coap.Message{
Type: coap.Acknowledgement,
Code: coap.NotFound,
}
}
return nil
}
func (ca *CoAPAdapter) COAPServer() *mux.Router {
r := mux.NewRouter()
r.Handle("/channels/{channel_id}/messages", coap.FuncHandler(ca.sendMessage)).Methods(coap.POST)
r.Handle("/channels/{channel_id}/messages", coap.FuncHandler(ca.observeMessage)).Methods(coap.GET)
r.NotFoundHandler = coap.FuncHandler(notFoundHandler)
return r
}
+7
View File
@@ -1,3 +1,10 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
// Package mainflux acts as an umbrella package containing multiple different
// microservices and defines all shared domain concepts.
package mainflux
-3
View File
@@ -1,3 +0,0 @@
docker-compose.yml
nginx.conf
ssl/
+2 -3
View File
@@ -1,11 +1,10 @@
FROM golang:1.9-alpine AS builder
FROM golang:1.10-alpine AS builder
ARG SVC_NAME
WORKDIR /go/src/github.com/mainflux/mainflux
COPY . .
RUN apk update \
&& apk add make protobuf git \
&& go get -u github.com/golang/protobuf/protoc-gen-go \
&& apk add make\
&& make $SVC_NAME \
&& mv build/mainflux-$SVC_NAME /exe
+4
View File
@@ -0,0 +1,4 @@
FROM scratch
ARG SVC_NAME
COPY mainflux-$SVC_NAME /exe
ENTRYPOINT ["/exe"]
@@ -0,0 +1,39 @@
version: "3"
networks:
docker_mainflux-base-net:
external: true
services:
bootstrap-db:
image: postgres:10.2-alpine
container_name: mainflux-bootstrap-db
restart: on-failure
environment:
POSTGRES_USER: mainflux
POSTGRES_PASSWORD: mainflux
POSTGRES_DB: bootstrap
networks:
- docker_mainflux-base-net
bootstrap:
image: mainflux/bootstrap:latest
container_name: mainflux-bootstrap
depends_on:
- bootstrap-db
restart: on-failure
ports:
- 8200:8200
environment:
MF_BOOTSTRAP_LOG_LEVEL: debug
MF_BOOTSTRAP_DB_HOST: bootstrap-db
MF_BOOTSTRAP_DB_PORT: 5432
MF_BOOTSTRAP_DB_USER: mainflux
MF_BOOTSTRAP_DB_PASS: mainflux
MF_BOOTSTRAP_DB: bootstrap
MF_BOOTSTRAP_DB_SSL_MODE: disable
MF_BOOTSTRAP_PORT: 8200
MF_SDK_BASE_URL: http://mainflux-things:8182
MF_USERS_URL: mainflux-users:8181
networks:
- docker_mainflux-base-net
@@ -0,0 +1,29 @@
###
# This docker-compose file contains optional Cassandra and cassandra-reader. Since these are optional, this file is
# dependent of docker-compose file from <project_root>/docker. In order to run
# these optional service, execute command:
# docker-compose -f docker/docker-compose.yml -f docker/addons/cassandra-reader/docker-compose.yml up
# from project root.
###
version: "3"
networks:
docker_mainflux-base-net:
external: true
services:
cassandra-reader:
image: mainflux/cassandra-reader:latest
container_name: mainflux-cassandra-reader
restart: on-failure
environment:
MF_CASSANDRA_READER_LOG_LEVEL: debug
MF_THINGS_URL: things:8183
MF_CASSANDRA_READER_PORT: 8903
MF_CASSANDRA_READER_DB_CLUSTER: mainflux-cassandra
MF_CASSANDRA_READER_DB_KEYSPACE: mainflux
ports:
- 8903:8903
networks:
- docker_mainflux-base-net
@@ -0,0 +1,40 @@
###
# This docker-compose file contains optional Cassandra and cassandra-writer. Since these are optional, this file is
# dependent of docker-compose file from <project_root>/docker. In order to run
# these optional service, execute command:
# docker-compose -f docker/docker-compose.yml -f docker/addons/cassandra-writer/docker-compose.yml up
# from project root.
###
version: "3"
networks:
docker_mainflux-base-net:
external: true
services:
cassandra:
image: cassandra:3.11.3
container_name: mainflux-cassandra
restart: on-failure
networks:
- docker_mainflux-base-net
ports:
- 9042:9042
cassandra-writer:
image: mainflux/cassandra-writer:latest
container_name: mainflux-cassandra-writer
depends_on:
- cassandra
restart: on-failure
environment:
MF_CASSANDRA_WRITER_LOG_LEVEL: debug
MF_NATS_URL: nats://nats:4222
MF_CASSANDRA_WRITER_PORT: 8902
MF_CASSANDRA_WRITER_DB_CLUSTER: mainflux-cassandra
MF_CASSANDRA_WRITER_DB_KEYSPACE: mainflux
ports:
- 8902:8902
networks:
- docker_mainflux-base-net
+3
View File
@@ -0,0 +1,3 @@
docker-compose -f docker/addons/cassandra-writer/docker-compose.yml up -d
sleep 20
docker exec mainflux-cassandra cqlsh -e "CREATE KEYSPACE IF NOT EXISTS mainflux WITH replication = {'class':'SimpleStrategy','replication_factor':'1'};"
@@ -0,0 +1,31 @@
###
# This docker-compose file contains optional InfluxDB-reader service for the Mainflux
# platform. Since this service is optional, this file is dependent on the docker-compose.yml
# file from <project_root>/docker/. In order to run InfluxDB-reader service, core services,
# as well as the network from the core composition, should be already running.
###
version: "3"
networks:
docker_mainflux-base-net:
external: true
services:
influxdb-reader:
image: mainflux/influxdb-reader:latest
container_name: mainflux-influxdb-reader
restart: on-failure
environment:
MF_INFLUX_READER_LOG_LEVEL: debug
MF_THINGS_URL: things:8183
MF_INFLUX_READER_PORT: 8905
MF_INFLUX_READER_DB_NAME: mainflux
MF_INFLUX_READER_DB_HOST: mainflux-influxdb
MF_INFLUX_READER_DB_PORT: 8086
MF_INFLUX_READER_DB_USER: mainflux
MF_INFLUX_READER_DB_PASS: mainflux
ports:
- 8905:8905
networks:
- docker_mainflux-base-net
@@ -0,0 +1,60 @@
###
# This docker-compose file contains optional InfluxDB, InfluxDB-writer and Grafana services
# for the Mainflux platform. Since this services are optional, this file is dependent on the
# docker-compose.yml file from <project_root>/docker/. In order to run these services,
# core services, as well as the network from the core composition, should be already running.
###
version: "3"
networks:
docker_mainflux-base-net:
external: true
services:
influxdb:
image: influxdb:1.6.4-alpine
container_name: mainflux-influxdb
restart: on-failure
environment:
INFLUXDB_DB: mainflux
INFLUXDB_ADMIN_USER: mainflux
INFLUXDB_ADMIN_PASSWORD: mainflux
networks:
- docker_mainflux-base-net
ports:
- 8086:8086
influxdb-writer:
image: mainflux/influxdb-writer:latest
container_name: mainflux-influxdb-writer
depends_on:
- influxdb
restart: on-failure
environment:
MF_INFLUX_WRITER_LOG_LEVEL: debug
MF_NATS_URL: nats://nats:4222
MF_INFLUX_WRITER_PORT: 8900
MF_INFLUX_WRITER_BATCH_SIZE: 5000
MF_INFLUX_WRITER_BATCH_TIMEOUT: 5
MF_INFLUX_WRITER_DB_NAME: mainflux
MF_INFLUX_WRITER_DB_HOST: mainflux-influxdb
MF_INFLUX_WRITER_DB_PORT: 8086
MF_INFLUX_WRITER_DB_USER: mainflux
MF_INFLUX_WRITER_DB_PASS: mainflux
ports:
- 8900:8900
networks:
- docker_mainflux-base-net
grafana:
image: grafana/grafana:5.1.3
container_name: mainflux-grafana
depends_on:
- influxdb
restart: on-failure
ports:
- 3001:3000
networks:
- docker_mainflux-base-net
@@ -0,0 +1,36 @@
###
# This docker-compose file contains optional lora-adapter and lora-redis services
# for the Mainflux platform. Since this services are optional, this file is dependent on the
# docker-compose.yml file from <project_root>/docker/. In order to run these services,
# core services, as well as the network from the core composition, should be already running.
###
version: "3"
networks:
docker_mainflux-base-net:
external: true
services:
lora-redis:
image: redis:5.0-alpine
container_name: mainflux-lora-redis
restart: on-failure
networks:
- docker_mainflux-base-net
lora-adapter:
image: mainflux/lora:latest
container_name: mainflux-lora
restart: on-failure
environment:
MF_LORA_ADAPTER_LOG_LEVEL: debug
MF_THINGS_ES_URL: things-redis:6379
MF_LORA_ADAPTER_ROUTEMAP_URL: lora-redis:6379
MF_LORA_ADAPTER_MESSAGES_URL: tcp://lora.mqtt.mainflux.io:1883
MF_LORA_ADAPTER_HTTP_PORT: 8187
MF_NATS_URL: nats://nats:4222
ports:
- 8187:8187
networks:
- docker_mainflux-base-net
@@ -0,0 +1,30 @@
###
# This docker-compose file contains optional MongoDB-reader service
# for Mainflux platform. Since these are optional, this file is dependent of docker-compose file
# from <project_root>/docker. In order to run these optional service, execute command:
# docker-compose -f docker/docker-compose.yml -f docker/addons/mongodb-reader/docker-compose.yml up
# from project root. MongoDB service is defined in docker/addons/mongodb-writer/docker-compose.yml.
###
version: "3"
networks:
docker_mainflux-base-net:
external: true
services:
mongodb-reader:
image: mainflux/mongodb-reader:latest
container_name: mainflux-mongodb-reader
restart: on-failure
environment:
MF_MONGO_READER_LOG_LEVEL: debug
MF_THINGS_URL: things:8183
MF_MONGO_READER_PORT: 8904
MF_MONGO_READER_DB_NAME: mainflux
MF_MONGO_READER_DB_HOST: mongodb
MF_MONGO_READER_DB_PORT: 27017
ports:
- 8904:8904
networks:
- docker_mainflux-base-net
@@ -0,0 +1,44 @@
###
# This docker-compose file contains optional MongoDB and MongoDB-writer services
# for Mainflux platform. Since these are optional, this file is dependent of docker-compose file
# from <project_root>/docker. In order to run these optional service, execute command:
# docker-compose -f docker/docker-compose.yml -f docker/addons/mongodb-writer/docker-compose.yml up
# from project root. MongoDB default port (27017) is exposed, so you can use various tools for database
# inspection and data visualization.
###
version: "3"
networks:
docker_mainflux-base-net:
external: true
services:
mongodb:
image: mongo:3.6-jessie
container_name: mainflux-mongodb
restart: on-failure
environment:
MONGO_INITDB_DATABASE: mainflux
ports:
- 27017:27017
networks:
- docker_mainflux-base-net
mongodb-writer:
image: mainflux/mongodb-writer:latest
container_name: mainflux-mongodb-writer
depends_on:
- mongodb
restart: on-failure
environment:
MF_MONGO_WRITER_LOG_LEVEL: debug
MF_NATS_URL: nats://nats:4222
MF_MONGO_WRITER_PORT: 8901
MF_MONGO_WRITER_DB_NAME: mainflux
MF_MONGO_WRITER_DB_HOST: mongodb
MF_MONGO_WRITER_DB_PORT: 27017
ports:
- 8901:8901
networks:
- docker_mainflux-base-net
+177 -50
View File
@@ -8,88 +8,215 @@
version: "3"
networks:
mainflux-base-net:
driver: bridge
services:
nginx:
image: nginx:1.13-alpine
image: nginx:1.14.2-alpine
container_name: mainflux-nginx
restart:
on-failure
restart: on-failure
volumes:
- $PWD/nginx.conf:/etc/nginx/nginx.conf
- $PWD/ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
- $PWD/ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
- $PWD/ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
- ./ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
- ./ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
ports:
- "80:80"
- "443:443"
- "8883:8883"
- 80:80
- 443:443
- 8883:8883
networks:
- mainflux-base-net
nats:
image: nats:1.0.2
image: nats:1.3.0
container_name: mainflux-nats
restart:
on-failure
restart: on-failure
networks:
- mainflux-base-net
postgres:
users-db:
image: postgres:10.2-alpine
container_name: mainflux-postgres
restart:
on-failure
container_name: mainflux-users-db
restart: on-failure
environment:
POSTGRES_USER: mainflux
POSTGRES_PASSWORD: mainflux
POSTGRES_DB: mainflux
ports:
- "5432:5432"
POSTGRES_DB: users
networks:
- mainflux-base-net
manager:
image: mainflux/manager:latest
container_name: mainflux-manager
users:
image: mainflux/users:latest
container_name: mainflux-users
depends_on:
- users-db
expose:
- 8180
restart:
on-failure
- 8181
restart: on-failure
environment:
MF_DB_HOST: postgres
MF_MANAGER_DB: mainflux
MF_MANAGER_PORT: 8180
MF_MANAGER_SECRET: test-secret
MF_USERS_LOG_LEVEL: debug
MF_USERS_DB_HOST: users-db
MF_USERS_DB_PORT: 5432
MF_USERS_DB_USER: mainflux
MF_USERS_DB_PASS: mainflux
MF_USERS_DB: users
MF_USERS_HTTP_PORT: 8180
MF_USERS_GRPC_PORT: 8181
MF_USERS_SECRET: secret
ports:
- 8180:8180
networks:
- mainflux-base-net
things-db:
image: postgres:10.2-alpine
container_name: mainflux-things-db
restart: on-failure
environment:
POSTGRES_USER: mainflux
POSTGRES_PASSWORD: mainflux
POSTGRES_DB: things
networks:
- mainflux-base-net
things-redis:
image: redis:5.0-alpine
container_name: mainflux-things-redis
restart: on-failure
networks:
- mainflux-base-net
things:
image: mainflux/things:latest
container_name: mainflux-things
depends_on:
- things-db
- users
restart: on-failure
environment:
MF_THINGS_LOG_LEVEL: debug
MF_THINGS_DB_HOST: things-db
MF_THINGS_DB_PORT: 5432
MF_THINGS_DB_USER: mainflux
MF_THINGS_DB_PASS: mainflux
MF_THINGS_DB: things
MF_THINGS_CACHE_URL: things-redis:6379
MF_THINGS_ES_URL: things-redis:6379
MF_THINGS_HTTP_PORT: 8182
MF_THINGS_GRPC_PORT: 8183
MF_USERS_URL: users:8181
MF_THINGS_SECRET: secret
ports:
- 8182:8182
- 8183:8183
networks:
- mainflux-base-net
normalizer:
image: mainflux/normalizer:latest
container_name: mainflux-normalizer
restart:
on-failure
restart: on-failure
depends_on:
- nats
expose:
- 8181
- 8184
environment:
MF_NATS_URL: "nats://nats:4222"
MF_NORMALIZER_PORT: 8181
MF_NORMALIZER_LOG_LEVEL: debug
MF_NATS_URL: nats://nats:4222
MF_NORMALIZER_PORT: 8184
ports:
- 8184:8184
networks:
- mainflux-base-net
ui:
image: mainflux/ui:latest
container_name: mainflux-ui
restart: on-failure
ports:
- 3000:3000
networks:
- mainflux-base-net
ws-adapter:
image: mainflux/ws:latest
container_name: mainflux-ws
depends_on:
- things
- nats
restart: on-failure
environment:
MF_WS_ADAPTER_LOG_LEVEL: debug
MF_WS_ADAPTER_PORT: 8186
MF_NATS_URL: nats://nats:4222
MF_THINGS_URL: things:8183
ports:
- 8186:8186
networks:
- mainflux-base-net
http-adapter:
image: mainflux/http:latest
container_name: mainflux-http
depends_on:
- manager
restart:
on-failure
- things
- nats
restart: on-failure
expose:
- 8182
- 8185
environment:
MF_MANAGER_URL: "http://manager:8180"
MF_NATS_URL: "nats://nats:4222"
MF_HTTP_ADAPTER_PORT: 8182
MF_HTTP_ADAPTER_LOG_LEVEL: debug
MF_HTTP_ADAPTER_PORT: 8185
MF_NATS_URL: nats://nats:4222
MF_THINGS_URL: things:8183
ports:
- 8185:8185
networks:
- mainflux-base-net
redis:
image: redis:4.0.9-alpine
container_name: mainflux-redis
restart: on-failure
networks:
- mainflux-base-net
mqtt-adapter:
image: mainflux/mqtt-adapter:latest
image: mainflux/mqtt:latest
container_name: mainflux-mqtt
depends_on:
- manager
restart:
on-failure
- things
- nats
- redis
restart: on-failure
environment:
MQTT_ADAPTER_NATS_URL: "nats://nats:4222"
AUTH_URL: "http://manager"
AUTH_PORT: 8180
MF_MQTT_ADAPTER_LOG_LEVEL: debug
MF_MQTT_ADAPTER_PORT: 1883
MF_MQTT_ADAPTER_WS_PORT: 8880
MF_MQTT_ADAPTER_REDIS_HOST: redis
MF_NATS_URL: nats://nats:4222
MF_THINGS_URL: things:8183
ports:
- "1883:1883"
- 1883:1883
- 8880:8880
networks:
- mainflux-base-net
coap-adapter:
image: mainflux/coap:latest
container_name: mainflux-coap
depends_on:
- things
- nats
restart: on-failure
environment:
MF_COAP_ADAPTER_LOG_LEVEL: debug
MF_COAP_ADAPTER_PORT: 5683
MF_NATS_URL: nats://nats:4222
MF_THINGS_URL: things:8183
ports:
- 5683:5683/udp
networks:
- mainflux-base-net
+132 -48
View File
@@ -54,29 +54,14 @@ http {
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
upstream docker-manager {
server mainflux-manager:8180;
}
upstream docker-http {
server mainflux-http:8182;
}
##
# Virtual Host Configs
##
# HTTP
# HTTPS
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name localhost;
access_log off;
error_log off;
return 301 https://$server_name$request_uri;
}
# HTTPS
server {
# SSL configuration
#
listen 443 ssl http2 default_server;
@@ -123,65 +108,164 @@ http {
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Headers "*";
server_name localhost;
# Proxy pass to manager service
# Proxy pass to users service
location ~ ^/(users|tokens) {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://users:8180;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to things service
location ~ ^/(things|channels) {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Access-Control-Expose-Headers Location;
proxy_pass http://things:8182;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
location /version {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://things:8182;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-http-adapter
location /http/ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://http-adapter:8185/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-ws-adapter
location /ws/ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
proxy_pass http://ws-adapter:8186/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-mqtt-adapter
location /mqtt {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
proxy_pass http://mqtt-adapter:8880/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://docker-manager;
proxy_pass http://ui:3000/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-http-adapter
location ~ ^/channels/[a-zA-Z0-9-]+/messages$ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://docker-http;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
}
}
###
# MQTT
###
stream {
upstream docker-mqtt {
server mainflux-mqtt:1883;
}
# MQTT
server {
listen 8883 ssl;
proxy_pass docker-mqtt;
listen 8883 ssl;
listen [::]:8883 ssl;
# Certificates
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
# from https://cipherli.st/
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
proxy_pass mqtt-adapter:1883;
}
}
+17 -17
View File
@@ -1,21 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDhDCCAmygAwIBAgIJAPOWQ/k/52/dMA0GCSqGSIb3DQEBDQUAMFcxEjAQBgNV
MIIDhDCCAmygAwIBAgIJAK61I+11kc4nMA0GCSqGSIb3DQEBDQUAMFcxEjAQBgNV
BAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsMA0lvVDEg
MB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb20wHhcNMTcwMTA3MDA0MzE4
WhcNMzIwMTA0MDA0MzE4WjBXMRIwEAYDVQQDDAlsb2NhbGhvc3QxETAPBgNVBAoM
MB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb20wHhcNMTgxMTAyMTc1MTEx
WhcNMzIxMDI5MTc1MTExWjBXMRIwEAYDVQQDDAlsb2NhbGhvc3QxETAPBgNVBAoM
CE1haW5mbHV4MQwwCgYDVQQLDANJb1QxIDAeBgkqhkiG9w0BCQEWEWluZm9AbWFp
bmZsdXguY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzOueTshY
eIDpcedEB+QcgmON72NXVkZ9s9rbNMCCLRoEl947m0SBfz4RXXXRh/plHNgEInsC
Z90LpKhyo5X8U1FIzTNefg3iMdov+wOGnGQJHydLSEbzWCDHjP54+W7XlQaA4/GT
76Os4m6e5LyLdb6jIQQx7EzZnJFQ9g85eldqEGz/tBn6eDLpHSt0ZidClUEGIJcV
5eeGRxKTrExnD1mSkDa63/s6M4fxkacxrwxnwVUdnSvHDxTOt5Mctrqlun63Sn+W
+9Afr5fZUdgROLkLhxWRVUbXMvh3OI5zMHpcONCvIr+gMEeM4q+AyVjCzYzMrDZ7
pvuhM4PGeAMAIwIDAQABo1MwUTAdBgNVHQ4EFgQUiBGMTsVJvNuSKvwoi/7aEvGf
RX0wHwYDVR0jBBgwFoAUiBGMTsVJvNuSKvwoi/7aEvGfRX0wDwYDVR0TAQH/BAUw
AwEB/zANBgkqhkiG9w0BAQ0FAAOCAQEARgi7Cr4JJmoOuP795OYxpVxZBxqnHOMi
2qFeiAwlg3M310YvZaJoewVK44hGNKtRbxc1CelEhRMCFabU5/xQOJmmx3bogNUc
944IfreJyJuUy7q3/6Ix3jf/rH1dpUVZ0S6baldIPKPzNNvmO6VFfmb+dymrMPCM
VQ4w+XoipLS5lPvG4z2/xz2auEAM5qtVjWQFB51Ju63bqKDQTwi+TaQSAu+9Dwr+
10w7qRZogM0anb8Q4Aq14Y7kplnu22VTVHaTyWb5FW4loWKP8FabzNCTOWneidVV
ApEfujCy2Bxw+bNxDtIp1ovPv2vljtJfdnFAKfBElw9BSPg/exUMHw==
bmZsdXguY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuTSZgdq0
BRjxYCNZV4QOAoN9eaoXSp5yD0DjdBfP4T5HZJYMmw1ca/EeO9raO1fOvAQ7CsCq
thKb8pw9HgPBLBL8GACTK/x2rjco9c2TbgTk7f6vSVoI+UUiU2Y5g4iIPIywYoGD
/WwCn38r7NyKxxzbEstnJ2TvcvoOBXTc0EfLJ3gox1Q+FlhYQ928FFSv78tWeXug
0X2KJKY8qUrXF1LDVLwv/93XduN+nGMJfoAYJQXRVmcPLtMFEycD90xUkKIPOPox
sSQwQl9RaM7r3M2ZsEirJpeJbvzVBxLdWWaG8HqT9oRZPrJZZvcpFgzX1Yf5oi2k
GMzSsYxJBnVsqwIDAQABo1MwUTAdBgNVHQ4EFgQUMmE/LCiFwPSKXDSAQvcRF/9y
M54wHwYDVR0jBBgwFoAUMmE/LCiFwPSKXDSAQvcRF/9yM54wDwYDVR0TAQH/BAUw
AwEB/zANBgkqhkiG9w0BAQ0FAAOCAQEACmbTBIq8cZebTa+IE8zUAj8KpaGLCn+7
nET6DYQzT1GoGToMVOdQ0goaGAGMhTGh8ezOxAPJoo3IYZwSErxSpyd20jASKkQG
p2Q+gBDZiohEumQkA2K6ywgTrVr/qNhGBvv+r40h3lJd2bbspfPLUq2zNnJpRhww
0QjObMnaDdXgD8kPy7poEUVmGxAYKhSBvi7gNInymaspGGwubNVrsakAjsi710r1
41KT4Pq4FpfHzqpSrrGq4VFbi1NSUZWGCIqIm+oYlA5l7/cMVPS7qtV/ScsMod8s
KSkNneFU0RqKeY7dMU2bkxlCcH+xUAmWefK9WFvwBJ4HjxE0Q83qPg==
-----END CERTIFICATE-----
+22 -23
View File
@@ -1,30 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFFTCCA/2gAwIBAgIJAPtBp1R03oQDMA0GCSqGSIb3DQEBDQUAMFcxEjAQBgNV
MIIFDDCCA/SgAwIBAgIJANUB7oWmhqk8MA0GCSqGSIb3DQEBDQUAMFcxEjAQBgNV
BAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsMA0lvVDEg
MB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb20wHhcNMTcwMTA3MDA1NTIw
WhcNMzIwMTA0MDA1NTIwWjBdMRgwFgYDVQQDDA9tYWluZmx1eC1zZXJ2ZXIxETAP
MB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb20wHhcNMTgxMTAyMTc1MzQ0
WhcNMzIxMDI5MTc1MzQ0WjBdMRgwFgYDVQQDDA9tYWluZmx1eC1zZXJ2ZXIxETAP
BgNVBAoMCE1haW5mbHV4MQwwCgYDVQQLDANJb1QxIDAeBgkqhkiG9w0BCQEWEWlu
Zm9AbWFpbmZsdXguY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
mGShbFfHhE/Q/CvVgpfas9o2B2H4E1nrDSmQXqwUPJ2U5+ExqpPkM/xX77yC9gut
k0RrM3fhB5AET9GnGMyFFlNYNPnWZOS6iQN2u5dg02TRqKdIhZZhs438u2wSRVwg
oAqYyiVpoV3QFa2BWTYbWwAhQGPXMUmUN8ZaUcWJ+s8PNhisj458hbWl7HuJ3ICB
xEAXiyd529YVHYJZVHyyEF5GwIvw3DOzZ++Ip6IVd0zYbdHw1pV5SAI5fbc4cb0C
cKXkSlqtjmz6ZeUWLJV98rYHc0YhUR7y2a6zNNFS/ROU6KEsWIdDTNDph0nHS1dB
sj4Abj2Kmf9InIPHHvqF+wIDAQABo4IB3DCCAdgwDAYDVR0TAQH/BAIwADARBglg
ovtHVZacXvf5m7iyhLdGqBaoTwUMSWR+LDaHyeUAcqLy4AHe2TGdJmbrsSKJOb94
Winru0dgQyBE1a/I+MOPYVNbYmShOoUSH6/a2Ph3DM8C7PjaFsc+uKd6NiVRmFid
c+3pQ62Um9mgJ5jD6kFB2uO5Bk9zxlDRGZz2BYvMFGcbhDZzf28O/Wwitfjb3dek
8kohknaIgHy50qstxt/GRFVHpqK0B+PVubC3Dr1Ext+lZORqM44o36jdUyVhnFXf
f8Fj/g4whrJfq3AOHeMsxm1VLqKeQb9CxpWvUi396w/bEXOuKXyoP5hyMUAvXEoE
pw2+M/CPHuCzj/ELafjWjQIDAQABo4IB0zCCAc8wDAYDVR0TAQH/BAIwADARBglg
hkgBhvhCAQEEBAMCBkAwCwYDVR0PBAQDAgXgMCoGCWCGSAGG+EIBDQQdFhtNYWlu
Zmx1eCBTZXJ2ZXIgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFB7OnWLMd24Eo5dcmJss
lLMkq59vMIGIBgNVHSMEgYAwfoAUiBGMTsVJvNuSKvwoi/7aEvGfRX2hW6RZMFcx
Zmx1eCBTZXJ2ZXIgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFDRwdZDGidKOpuJ29hXz
RWRFD8YkMIGIBgNVHSMEgYAwfoAUMmE/LCiFwPSKXDSAQvcRF/9yM56hW6RZMFcx
EjAQBgNVBAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsM
A0lvVDEgMB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb22CCQDzlkP5P+dv
3TBEBgNVHREEPTA7hwTAqAAuhxD+gAAAAAAAAGJsZv/+yw+7hwR/AAABhxAAAAAA
AAAAAAAAAAAAAAABgglsb2NhbGhvc3QwgYsGA1UdIASBgzCBgDB+BgMrBQgwdzAc
BggrBgEFBQcCARYQaHR0cDovL2xvY2FsaG9zdDBXBggrBgEFBQcCAjBLMA8WCE1h
aW5mbHV4MAMCAQEaOFRoaXMgQ0EgaXMgZm9yIGEgbG9jYWwgTWFpbmZsdXggc2Vy
dmVyIGluc3RhbGxhdGlvbiBvbmx5MA0GCSqGSIb3DQEBDQUAA4IBAQBxGCqJRbnw
lKHhpqZVEEWV1t87wQnf2qOV8SOzh1evF5sYeYEOnb2d802r0p08kuiJNETZdJOh
K/f7dVCL+mtSzHiK8SY8WJ8l0xfW+0qo/GW9jd9QDGbuwi6cRUw1lRhr5p/0ge9N
e3VlI7cjpG/Kv3x1AtCjMpMLzAxOLZmbSWgrMvtJIsMHcQTiV1HexIq9/A3XVthf
zuRUr1qyj3nx6ga2eHqaJQ5/Zu1A7zjHbZTiW4U5Ikl1PDWL3V0uEb3bXZ7xABb9
pjYjDA1Bm4eQMPJ+ZWRs5EFHBnLJc/Kz+4sfUuwiqI4xz2LMeCrdMbh0YSP/rhi4
wERrPpFmvpN3
A0lvVDEgMB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb22CCQCutSPtdZHO
JzA7BgNVHREENDAyhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABggV1c2Vyc4IGdGhp
bmdzgglsb2NhbGhvc3QwgYsGA1UdIASBgzCBgDB+BgMrBQgwdzAcBggrBgEFBQcC
ARYQaHR0cDovL2xvY2FsaG9zdDBXBggrBgEFBQcCAjBLMA8WCE1haW5mbHV4MAMC
AQEaOFRoaXMgQ0EgaXMgZm9yIGEgbG9jYWwgTWFpbmZsdXggc2VydmVyIGluc3Rh
bGxhdGlvbiBvbmx5MA0GCSqGSIb3DQEBDQUAA4IBAQAwrfzfLLe35aTGel9tWpWi
aWSATgYThCzaFqqzsQBNAmB/S+06xI2JSeXtHOestsqLZOrWdGG6LJcRiyCR/XWv
SxDRPUgjp14YQCml8GWQLairhdXNWZch1d+Bzr2XkJrTzQUex/XCJQnp56CzjFUO
XADhQdiaESvu/tk7nRuX8qYqwyIqwzRm5KlqHJIvsDXddGluS5EtsshdtAwbRQdR
jK1egJ3Z26vn68zrZiPQOYz9gmJs2Zl71bd4cGEBa6m7RGi9ww7gU6TsMoE8BTDg
i2ia+MB+COQB8ISx1+Pyx2migImZlnlYfSup1ObboyJV2jdqROBWbaosHqhh/7VF
-----END CERTIFICATE-----
+25 -25
View File
@@ -1,27 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAmGShbFfHhE/Q/CvVgpfas9o2B2H4E1nrDSmQXqwUPJ2U5+Ex
qpPkM/xX77yC9gutk0RrM3fhB5AET9GnGMyFFlNYNPnWZOS6iQN2u5dg02TRqKdI
hZZhs438u2wSRVwgoAqYyiVpoV3QFa2BWTYbWwAhQGPXMUmUN8ZaUcWJ+s8PNhis
j458hbWl7HuJ3ICBxEAXiyd529YVHYJZVHyyEF5GwIvw3DOzZ++Ip6IVd0zYbdHw
1pV5SAI5fbc4cb0CcKXkSlqtjmz6ZeUWLJV98rYHc0YhUR7y2a6zNNFS/ROU6KEs
WIdDTNDph0nHS1dBsj4Abj2Kmf9InIPHHvqF+wIDAQABAoIBAGyneyyrXXbqDcBu
ZHoLWYTYdaNH57+sYdnto6DMolUhqdS2jFnpvlCOgAhPaTSS2PxiUOjOdWSV+20J
t1EIKW/klsSWyZUAPDuKe7J+2St/+7h7JUsSELEb8HGVOWW4rQ5O3+dpS2ohYEbE
gbAg0tpMOmkVho3+vy4RP76D0MBAnhgl99fjo9jpNxPmBis+L/IJS5SNO1JFXMCf
rvyOIekmtmHBI4PgubVylwOIt03r867gi6WpSOBq5rTusDqwCdmDCeRWDL4EP3Zr
VH8EPbS1Zlcw0FH8yXjp2ts5UcJZL5CdLY7jJ6vkIiw64nNWx7cR4qd55ut280K4
tx4yqnkCgYEAxkQRZgxNHu+7KMUt7gjYRLaNhiSFrcwuSVb8HlhsYYYMH3GPQhvr
dQOOiibzsCna96GLbfdOpi9iwF82uuwNpdeKptPNb5Mht4oNqu5kKg1sl6S6fjSv
yrJACC0NcLRS2LYuBcylUCKsU7g3C9X6VmaSsh415v1s2qFHFik2AycCgYEAxMTt
kHP+pIwr35IvamRCGtUvI0D9R62gxxovpfQeetQMhc/3tOBJe/GOXDP4jitrz1l1
YMcLiCqktFxJqi6UKxcTwnGgcyPyC2UZSJJ/0lOIZDSP7JM7dT1xkrGeQyMjC09C
AwRi/ZAUUwLsMRxfTrj8igF8Md+LIjKBcYdm2w0CgYEAxg6hQsvvDoR09pli9HKp
eJrUbbh2QdPCOUlHuhiizBlYauDKN0QkxlOzRJb8wHJPZyhdXJC8ZI7Zm0qCJeBB
EfZrb5QNmPPlrq+eT66tKMUYQbQxCHohUd8W0BQRZRD94ba76tcwHQlGFKvlcVFk
LoNw77X2KrXm09BgbubkKekCgYAygztZMe3U4AcDRcvWTBaMPN309uIOXIxBkH9a
4uhQL89nKpQ0Yr96ifA5yz2rgYoTmKuBRJe5RPkzM93VSk/PIAV6jSDbbgbc1f8/
mhwmKjuBPd0UpldFKZjWR7KRGZwNczNHAwFGho4xITbxBI+S7fomk2sGgpR9GuoP
8up8oQKBgEMd9Mbo3xC0xW29V5P3FUKH4zRYJYlpk+30bZ4VCrgim9cVBgN/xrIA
l4yEnitEi1591b/r+Uz3b6yWOiLEHRE2U7sQLbfh2fvF0VhFCOE11FeAuoZp89/k
TVnL/FelAMbL1iZFIf9LRY2DdfAUKlO//cXMHcC2iatBUwfylSVS
MIIEpQIBAAKCAQEAovtHVZacXvf5m7iyhLdGqBaoTwUMSWR+LDaHyeUAcqLy4AHe
2TGdJmbrsSKJOb94Winru0dgQyBE1a/I+MOPYVNbYmShOoUSH6/a2Ph3DM8C7Pja
Fsc+uKd6NiVRmFidc+3pQ62Um9mgJ5jD6kFB2uO5Bk9zxlDRGZz2BYvMFGcbhDZz
f28O/Wwitfjb3dek8kohknaIgHy50qstxt/GRFVHpqK0B+PVubC3Dr1Ext+lZORq
M44o36jdUyVhnFXff8Fj/g4whrJfq3AOHeMsxm1VLqKeQb9CxpWvUi396w/bEXOu
KXyoP5hyMUAvXEoEpw2+M/CPHuCzj/ELafjWjQIDAQABAoIBACZM2KmjWCH1MkJG
+CS6ES+lC25uaEHDDOeE9Qob96v44QyFRAJUZ3LD0vVwgUEB3t2JZm0/S77akXKJ
+beD9WjQtvP7Y+wlo2mw4MQGN6vZ5f0cSdv6fKHWMaERygf8IxxAN4YA/6BUVw+X
FcsyBLDUvQFfoHxlN45GVYZeINpWNJEM5NkQpoz5JJmWplfRREtRDiFfdFvh8l25
nOeYaZkD9IAPW5k7ukzzUM4voNOKoutCV3h30AbD8zNMPblatLTrT9ZRtjEiofDn
iNT6DR2Ge4WWN8e+9h9CEklhhCwBULNlCg2mdNKsiU3q74oBExiKNrtJGxZXlhny
PIpciL0CgYEA01HmmXG1z82QmcvoMZBoHu9wNNl5krt5YsYUF9TWXTPijw6zxnLB
b0ef997KzMt1ZGRSq8Zae6YdMRXCvQJh1SMbhBTMfOwzbx+cqCcInmia47jAXgaV
agAc7im3gULeTnXtNRdo9cj2ilHmlJNFI0N0rl9NJxpYACEeSFaR6YMCgYEAxXD4
Z0+Boo+wsIvcxYWkYWpvhJfqqdETZJ5RdgkWLFG7OA2SWPgqIyWxPTnQRsK5ryjw
xyxNv7L9Ddd/+yCNsiuUIEdhLPtoIfBQ96722IJMTOxr9eWUXVngANBeMUypv/3q
v3AeD8MfTgJJPZ5F3zfLAeBPUxUx6JUIRxjFEq8CgYEAh+3q01EdjinAle1f0mH9
V8jU+GnbldMZ5btcOWgi65jwZu0iHsi6PIZqE9svwEQvowAVXYEduuPDa0uAFGxv
2dXXYUKvtruI6jX/YvqYxKys1UaGFvVNLv4bnecfrvoAXZ/lkX0ZeuBmFdtQ4slI
c8J0T6Xlzv1XFd43yHhHK1kCgYEAwwc0V9hRVSJahgmhqfq4xQE95tupENVVMq6w
CMgw/tY8+MFvLjL0bINu7+uLsFno0Py/2f4JTrKfBG06HfWqAbTKPJhFhQlRczTO
xdouOu96LwHaIqsEQrHkculgIZJ4mw1WNIOrLiavZX8cmaEdo8CY5uGLeaYWBogw
BQqSoEECgYEAuNXFrwDzUoqiFo9+7gjzT4pjQgh/zNTIODaXwIVBjTRe6Z4bQayd
Jel5Gf1IHu9iik6pvaVfK8tMc8eQisEC8F9U9l6mqw/Q4bpQ5k6CQtGe0roBEj2H
qJIQ/1TjMAOkOx7YiqTuFP4vs9LhCRAflQQ/Tg0fcHsMWBlPpWlEkRs=
-----END RSA PRIVATE KEY-----
+1 -1
View File
@@ -1 +1 @@
../.github/CONTRIBUTING.md
../CONTRIBUTING.md
+18 -18
View File
@@ -4,36 +4,36 @@ Mainflux IoT platform is comprised of the following services:
| Service | Description |
|:--------------------------------------------------------------------------|:------------------------------------------------------------------------|
| [manager](https://github.com/mainflux/mainflux/tree/master/manager) | Manages platform entities, and auth concerns |
| [http-adapter](https://github.com/mainflux/mainflux/tree/master/http) | Provides an HTTP interface for accessing communication channels |
| [users](https://github.com/mainflux/mainflux/tree/master/users) | Manages platform's users and auth concerns |
| [things](https://github.com/mainflux/mainflux/tree/master/things) | Manages platform's things, channels and access policies |
| [normalizer](https://github.com/mainflux/mainflux/tree/master/normalizer) | Normalizes SenML messages and generates the "processed" messages stream |
> The following diagram is an (obsolete) overview of platform architecture
| [http-adapter](https://github.com/mainflux/mainflux/tree/master/http) | Provides an HTTP interface for accessing communication channels |
| [ws-adapter](https://github.com/mainflux/mainflux/tree/master/ws) | Provides a WebSocket interface for accessing communication channels |
| [mqtt-adapter](https://github.com/mainflux/mainflux/tree/master/mqtt) | Provides an MQTT interface for accessing communication channels |
| [coap-adapter](https://github.com/mainflux/mainflux/tree/master/coap) | Provides a CoAP interface for accessing communication channels |
| [lora-adapter](https://github.com/mainflux/mainflux/tree/master/lora) | Provides a LoRa Server forwarder for accessing communication channels |
| [mainflux-cli](https://github.com/mainflux/mainflux/tree/master/cli) | Command line interface |
![arch](img/architecture.jpg)
## Domain model
The platform is built around 3 main entities: **users**, **clients** and **channels**.
The platform is built around 3 main entities: **users**, **things** and **channels**.
`User` represents the real (human) user of the system. It is represented via its
e-mail and password, which he uses as platform access credentials in order to obtain
an access token. Once logged into the system, user can manage his resources in
CRUD fashion (i.e. channels and clients), and define access control policies
between them.
an access token. Once logged into the system, user can manage his resources (i.e.
things and channels) in CRUD fashion, and define access control policies by
connecting them.
`Device` is used to represent any device that connects to Mainflux. It is a
generic model that describes any client device of the system.
`Application` is very similar to the `Device` and is represented by the same
`Client` structure (just with different `type` info). Application represents
an end-user application that communicates with devices through Mainflux, and
can be running somewhere in the cloud, locally on the PC or on the mobile phone.
Usually it acquires data from sensor measurement and displays it on various
dashboards.
`Thing` represents devices and applications connected to Mainflux. There are
two types of "things" supported at the moment: `device` and `app`. While device
is used to represent any physical device connected to the platform, app represents
any 3rd party service that uses the platform for message exchange with other
"things".
`Channel` represents a communication channel. It serves as message topic that
can be consumed by all of the clients connected to it.
can be consumed by all of the things connected to it.
## Messaging
+90
View File
@@ -0,0 +1,90 @@
## Bootstrap
`Bootstrapping` refers to a self-starting process that is supposed to proceed without external input.
Mainflux platform supports bootstrapping process, but some of the preconditions need to be fulfilled in advance. The device can trigger a bootstrap when:
- device contains only bootstrap credentials and no Mainflux credentials
- device, for any reason, fails to start a communication with the configured Mainflux services (server not responding, authentication failure, etc..).
- device, for any reason, wants to update its configuration
> Bootstrapping and provisioning are two different procedures. Provisioning refers to entities management while bootstrapping is related to entity configuration.
![bootstrapping flow](img/bs_flow.gif)
### Configuration
The configuration of Mainflux thing consists of two major parts:
- The list of Mainflux channels the thing is connected to
- Custom configuration related to the specific thing
Also, the configuration contains an external ID and external key, which will be explained later.
In order to enable the thing to start bootstrapping process, the user needs to upload a valid configuration for that specific thing. This can be done using the following HTTP request:
```
curl -s -S -i -X POST -H "Authorization: <user_token>" -H "Content-Type: application/json" http://localhost:8200/things/configs -d '{
"external_id":"09:6:0:sb:sa",
"thing_id": "1b9b8fae-9035-4969-a240-7fe5bdc0ed28",
"external_key":"key",
"name":"some",
"channels":[
"c3642289-501d-4974-82f2-ecccc71b2d83",
"cd4ce940-9173-43e3-86f7-f788e055eb14",
"ff13ca9c-7322-4c28-a25c-4fe5c7b753fc",
"c3642289-501d-4974-82f2-ecccc71b2d82"
],
"content": "config..."
}'
```
In this example, `channels` field represents the list of Mainflux channel IDs the thing is connected to. These channels need to be provisioned before the configuration is uploaded. Field `content` represents custom configuration. This custom configuration contains parameters that can be used to set up the thing. It can also be empty if no additional set up is needed. Field `name` is human readable name and `thing_id` is an ID of the Mainflux thing. This field is not required. If `thing_id` is empty, corresponding Mainflux thing will be created implicitly and its ID will be sent as a part of `Location` header of the response.
There are two more fields: `external_id` and `external_key`. External ID represents an ID of the device that corresponds to the given thing. For example, this can be a MAC address or the serial number of the device. The external key represents the device key. This is the secret key that's safely stored on the device and it is used to authorize the thing during the bootstrapping process. Please note that external ID and external key and Mainflux ID and Mainflux key are _completely different concepts_. External id and key are only used to authenticate a device that corresponds to the specific Mainflux thing during the bootstrapping procedure.
### Bootstrapping
Currently, the bootstrapping procedure is executed over the HTTP protocol. Bootstrapping is nothing else but fetching and applying the configuration that corresponds to the given Mainflux thing. In order to fetch the configuration, _the thing_ needs to send a bootstrapping request:
```
curl -s -S -i -H "Authorization: <external_key>" http://localhost:8200/things/bootstrap/<external_id>
```
The response body should look something like:
```
{
"mainflux_id":"7c9df5eb-d06b-4402-8c1a-df476e4394c8",
"mainflux_key":"86a4f870-eba4-46a0-bef9-d94db2b64392",
"mainflux_channels":[
{
"id":"ff13ca9c-7322-4c28-a25c-4fe5c7b753fc",
"name":"some channel",
"metadata":{
"operation":"someop",
"type":"metadata"
}
},
{
"id":"925461e6-edfb-4755-9242-8a57199b90a5",
"name":"channel1",
"metadata":{
"type":"control"
}
}
],
"content":"config..."
}
```
The response consists of an ID and key of the Mainflux thing, the list of channels and custom configuration (`content` field). The list of channels contains not just channel IDs, but the additional Mainflux channel data (`name` and `metadata` fields), as well.
### Enabling and disabling things
Uploading configuration does not automatically connect thing to the given list of channels. In order to connect the thing to the channels, user needs to send the following HTTP request:
```
curl -s -S -i -X PUT -H "Authorization: <user_token>" -H "Content-Type: application/json" http://localhost:8200/things/state/<thing_id> -d '{"state": 1}'
```
In order to disconnect, the same request should be sent with the value of `state` set to 0.
For more information about Bootstrap API, please check out the [API documentation](https://github.com/mainflux/mainflux/blob/master/bootstrap/swagger.yml).
+165
View File
@@ -0,0 +1,165 @@
## CLI
Mainflux CLI makes it easy to manage users, things, channels and messages.
CLI can be downloaded as separate asset from [project realeses](https://github.com/mainflux/mainflux/releases) or it can be built with `GNU Make` tool:
```
make cli
```
which will build `mainflux-cli` in `<project_root>/build` folder.
Executing `build/mainflux-cli` without any arguments will output help with all available commands and flags:
```
Usage:
mainflux-cli [command]
Available Commands:
channels Channels management
help Help about any command
messages Send or read messages
provision Provision things and channels from config file
things Things management
users Users management
version Mainflux system version
Flags:
-c, --content-type string Mainflux message content type (default "application/senml+json")
-h, --help help for mainflux-cli
-a, --http-prefix string Mainflux http adapter prefix (default "http")
-i, --insecure Do not check for TLS cert
-l, --limit uint limit query parameter (default 100)
-m, --mainflux-url string Mainflux host URL (default "http://localhost")
-o, --offset uint offset query parameter
-t, --things-prefix string Mainflux things service prefix
-u, --users-prefix string Mainflux users service prefix
Use "mainflux-cli [command] --help" for more information about a command.
```
You can execute each command with `-h` flag for more information about that command, e.g.
```
./mainflux-cli channels -h
```
will get you usage info:
```
Channels management: create, get, update or delete Channels and get list of Things connected to Channels
Usage:
mainflux-cli channels [flags]
mainflux-cli channels [command]
Available Commands:
connections connections <channel_id> <user_auth_token>
create create <JSON_channel> <user_auth_token>
delete delete <channel_id> <user_auth_token>
get get <channel_id | all> <user_auth_token>
update update <JSON_string> <user_auth_token>
```
## Service
#### Get the version of Mainflux services
```
mainflux-cli version
```
### Users management
#### Create User
```
mainflux-cli users create john.doe@email.com password
```
#### Login User
```
mainflux-cli users token john.doe@email.com password
```
### System Provisioning
#### Create Thing (type Device)
```
mainflux-cli things create '{"type":"device", "name":"myDevice"}' <user_auth_token>
```
#### Create Thing (type Application)
```
mainflux-cli things create '{"type":"app", "name":"myDevice"}' <user_auth_token>
```
#### Update Thing
```
mainflux-cli things update '{"id":"<thing_id>", "name":"myNewName"}' <user_auth_token>
```
#### Remove Thing
```
mainflux-cli things delete <thing_id> <user_auth_token>
```
#### Retrieve a subset list of provisioned Things
```
mainflux-cli things get all --offset=1 --limit=5 <user_auth_token>
```
#### Retrieve Thing By ID
```
mainflux-cli things get <thing_id> <user_auth_token>
```
#### Create Channel
```
mainflux-cli channels create '{"name":"myChannel"}' <user_auth_token>
```
#### Update Channel
```
mainflux-cli channels update '{"id":"<channel_id>","name":"myNewName"}' <user_auth_token>
```
#### Remove Channel
```
mainflux-cli channels delete <channel_id> <user_auth_token>
```
#### Retrieve a subset list of provisioned Channels
```
mainflux-cli channels get all --offset=1 --limit=5 <user_auth_token>
```
#### Retrieve Channel By ID
```
mainflux-cli channels get <channel_id> <user_auth_token>
```
### Access control
#### Connect Thing to Channel
```
mainflux-cli things connect <thing_id> <channel_id> <user_auth_token>
```
#### Disconnect Thing from Channel
```
mainflux-cli things disconnect <thing_id> <channel_id> <user_auth_token>
```
#### Retrieve a subset list of Channels connected to Thing
```
mainflux-cli things connections <thing_id> <user_auth_token>
```
#### Retrieve a subset list of Things connected to Channel
```
mainflux-cli channels connections <channel_id> <user_auth_token>
```
### Messaging
#### Send a message over HTTP
```
mainflux-cli msg send <channel_id> '[{"bn":"Dev1","n":"temp","v":20}, {"n":"hum","v":40}, {"bn":"Dev2", "n":"temp","v":20}, {"n":"hum","v":40}]' <thing_auth_token>
```
+257
View File
@@ -0,0 +1,257 @@
## Getting Mainflux
Mainflux can be fetched from the official [Mainflux GitHub repository](https://github.com/Mainflux/mainflux):
```
go get github.com/mainflux/mainflux
cd $GOPATH/src/github.com/mainflux/mainflux
```
## Building
### Prerequisites
Make sure that you have [Protocol Buffers](https://developers.google.com/protocol-buffers/) compiler (`protoc`) installed.
[Go Protobuf](https://github.com/golang/protobuf) installation instructions are [here](https://github.com/golang/protobuf#installation).
Go Protobuf uses C bindings, so you will need to install [C++ protobuf](https://github.com/google/protobuf) as a prerequisite.
Mainflux uses `Protocol Buffers for Go with Gadgets` to generate faster marshaling and unmarshaling Go code. Protocol Buffers for Go with Gadgets instalation instructions can be found (here)(https://github.com/gogo/protobuf).
### Build All Services
Use `GNU Make` tool to build all Mainflux services:
```
make
```
Build artefacts will be put in the `build` directory.
> N.B. All Mainflux services are built as a statically linked binaries. This way they can be portable (transferred to any platform just by placing them there and running them) as they contain all needed libraries and do not relay on shared system libraries. This helps creating [FROM scratch](https://hub.docker.com/_/scratch/) dockers.
### Build Individual Microservice
Individual microservices can be built with:
```
make <microservice_name>
```
For example:
```
make http
```
will build the HTTP Adapter microservice.
### Building Dockers
Dockers can be built with:
```
make dockers
```
or individually with:
```
make docker_<microservice_name>
```
For example:
```
make docker_http
```
> N.B. Mainflux creates `FROM scratch` docker containers which are compact and small in size.
> N.B. The `things-db` and `users-db` containers are built from a vanilla PostgreSQL docker image downloaded from docker hub which does not persist the data when these containers are rebuilt. Thus, __rebuilding of all docker containers with `make dockers` or rebuilding the `things-db` and `users-db` containers separately with `make docker_things-db` and `make docker_users-db` respectively, will cause data loss. All your users, things, channels and connections between them will be lost!__ As we use this setup only for development, we don't guarantee any permanent data persistence. If you need to retain the data between the container rebuilds you can attach volume to the `things-db` and `users-db` containers. Check the official docs on how to use volumes [here](https://docs.docker.com/storage/volumes/) and [here](https://docs.docker.com/compose/compose-file/#volumes). For examples on how to add persistent volumes check the [Overriding the default docker-compose configuration](#overriding-the-default-docker-compose-configuration) section.
#### Building Docker images for development
In order to speed up build process, you can use commands such as:
```bash
make dockers_dev
```
or individually with
```bash
make docker_dev_<microservice_name>
```
Commands `make dockers` and `make dockers_dev` are similar. The main difference is that building images in the development mode is done on the local machine, rather than an intermediate image, which makes building images much faster. Before running this command, corresponding binary needs to be built in order to make changes visible. This can be done using `make` or `make <service_name>` command. Commands `make dockers_dev` and `make docker_dev_<service_name>` should be used only for development to speed up the process of image building. **For deployment images, commands from section above should be used.**
### Overriding the default docker-compose configuration
Sometimes, depending on the use case and the user's needs it might be useful to override or add some extra parameters to the docker-compose configuration. These configuration changes can be done by specifying multiple compose files with the [docker-compose command line option -f](https://docs.docker.com/compose/reference/overview/) as described [here](https://docs.docker.com/compose/extends/).
The following format of the `docker-compose` can be used to extend or override the configuration:
```
docker-compose -f docker/docker-compose.yml -f docker/docker-compose.custom1.yml -f docker/docker-compose.custom2.yml up [-d]
```
In the command above each successive file overrides the previous parameters.
A practical example in our case would be to add persistent volumes to the users-db, things-db, influxdb and grafana containers so that we don't loose data between updates of Mainflux. The volumes are mapped to the default location on the host machine, something like `/var/lib/docker/volumes/project-name_volume-name`:
```yaml
# docker/docker-compose.persistence.yml
version: "3"
volumes:
mainflux-users-db-volume:
mainflux-things-db-volume:
services:
users-db:
volumes:
- mainflux-users-db-volume:/var/lib/postgresql/data
things-db:
volumes:
- mainflux-things-db-volume:/var/lib/postgresql/data
```
```yaml
# docker/addons/influxdb-writer/docker-compose.persistence.yml
version: "3"
volumes:
influxdb-volume:
grafana-volume:
services:
influxdb:
volumes:
- influxdb-volume:/var/lib/influxdb
grafana:
volumes:
- grafana-volume:/var/lib/grafana
```
When we have the override files in place, to compose the whole infrastructure including the persistent volumes we can execute:
```
docker-compose -f docker/docker-compose.yml -f docker/docker-compose.persistence.yml up -d
docker-compose -f docker/addons/influxdb-writer/docker-compose.yml -f docker/addons/influxdb-writer/docker-compose.persistence.yml up -d
```
__Note:__ Please store your customizations to some folder outside the Mainflux's source folder and maybe add them to some other git repository. You can always apply your customizations by pointing to the right file using `docker-compose -f ...`. Also be sure not to use the `make cleandocker` and `make cleanghost` as they might delete something which you don't want to delete (i.e. the newly created persistent volumes).
### MQTT Microservice
The MQTT Microservice in Mainflux is special, as it is currently the only microservice written in NodeJS. It is not compiled, but node modules need to be downloaded in order to start the service:
```
cd mqtt
npm install
```
Note that there is a shorthand for doing these commands with `make` tool:
```
make mqtt
```
After that, the MQTT Adapter can be started from top directory (as it needs to find `*.proto` files) with:
```
node mqtt/mqtt.js
```
### Protobuf
If you've made any changes to `.proto` files, you should call `protoc` command prior to compiling individual microservices.
To do this by hand, execute:
```
protoc --gofast_out=plugins=grpc:. *.proto
```
A shorthand to do this via `make` tool is:
```
make proto
```
> N.B. This must be done once at the beginning in order to generate protobuf Go structures needed for the build. However, if you don't change any of `.proto` files, this step is not mandatory, since all generated files are included in the repo (those are files with `.pb.go` extension).
### Cross-compiling for ARM
Mainflux can be compiled for ARM platform and run on Raspberry Pi or other similar IoT gateways, by following the instructions [here](https://dave.cheney.net/2015/08/22/cross-compilation-with-go-1-5) or [here](https://www.alexruf.net/golang/arm/raspberrypi/2016/01/16/cross-compile-with-go-1-5-for-raspberry-pi.html) as well as information
found [here](https://github.com/golang/go/wiki/GoArm). The environment variables `GOARCH=arm` and `GOARM=7` must be set for the compilation.
Cross-compilation for ARM with Mainflux make:
```
GOOS=linux GOARCH=arm GOARM=7 make
```
## Running tests
To run all of the tests you can execute:
```
make test
```
Dockertest is used for the tests, so to run them, you will need the Docker daemon/service running.
## Installing
Installing Go binaries is simple: just move them from `build` to `$GOBIN` (do not fortget to add `$GOBIN` to your `$PATH`).
You can execute:
```
make install
```
which will do this copying of the binaries.
> N.B. Only Go binaries will be installed this way. The MQTT adapter is a NodeJS script and will stay in the `mqtt` dir.
## Deployment
### Prerequisites
Mainflux depends on several infrastructural services, notably [NATS](https://www.nats.io/) broker and [PostgreSQL](https://www.postgresql.org/) database.
#### NATS
Mainflux uses NATS as it's central message bus. For development purposes (when not run via Docker), it expects that NATS is installed on the local system.
To do this execute:
```
go get github.com/nats-io/gnatsd
```
This will install `gnatsd` binary that can be simply run by executing:
```
gnatsd
```
#### PostgreSQL
Mainflux uses PostgreSQL to store metadata (`users`, `things` and `channels` entities alongside with authorization tokens).
It expects that PostgreSQL DB is installed, set up and running on the local system.
Information how to set-up (prepare) PostgreSQL database can be found [here](https://support.rackspace.com/how-to/postgresql-creating-and-dropping-roles/),
and it is done by executing following commands:
```
# Create `users` and `things` databases
sudo -u postgres createdb users
sudo -u postgres createdb things
# Set-up Postgres roles
sudo su - postgres
psql -U postgres
postgres=# CREATE ROLE mainflux WITH LOGIN ENCRYPTED PASSWORD 'mainflux';
postgres=# ALTER USER mainflux WITH LOGIN ENCRYPTED PASSWORD 'mainflux';
```
### Mainflux Services
Running of the Mainflux microservices can be tricky, as there is a lot of them and each demand configuration in the form of environment variables.
The whole system (set of microservices) can be run with one command:
```
make rundev
```
which will properly configure and run all microservices.
Please assure that MQTT microservice has `node_modules` installed, as explained in _MQTT Microservice_ chapter.
> N.B. `make rundev` actually calls helper script `scripts/run.sh`, so you can inspect this script for the details.
+79 -254
View File
@@ -1,284 +1,109 @@
## Prerequisites
## Step 1 - Run the System
Before proceeding, install the following prerequisites:
- [Docker](https://docs.docker.com/install/)
- [Docker compose](https://docs.docker.com/compose/install/)
- [jsonpp](https://jmhodges.github.io/jsonpp/) (optional)
Once everything is installed, execute the following commands from project root:
Once everything is installed, execute the following command from project root:
```bash
docker-compose -f docker/docker-compose.yml up -d
make run
```
## User management
This will start Mainflux docker composition, which will output the logs from the containers.
### Account creation
## Step 2 - Install the CLI
Open a new terminal from which you can interact with the running Mainflux system. The easiest way to do this is by usin Mainflux CLI,
which can be downloaded as a tarball from GitHub (here we use release `0.7.0` but be sure to use the latest release):
Use the Mainflux API to create user account:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json; charset=utf-8" https://localhost/users -d '{"email":"john.doe@email.com", "password":"123"}'
```bash
wget -O- https://github.com/mainflux/mainflux/releases/download/0.7.0/mainflux-cli_v0.7.0_linux-amd64.tar.gz | tar xvz -C $GOBIN
```
Note that when using official `docker-compose`, all services are behind `nginx`
proxy and all traffic is `TLS` encrypted.
> Make sure that `$GOBIN` is added to your `$PATH` so that `mainflux-cli` command can be accessible system-wide
### Obtaining an authorization key
In order for this user to be able to authenticate to the system, you will have
to create an authorization token for him:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json; charset=utf-8" https://localhost/tokens -d '{"email":"john.doe@email.com", "password":"123"}'
## Step 3 - Provision the System
Once installed, you can use the CLI to quick-provision the system for testing:
```bash
mainflux-cli provision test
```
Response should look like this:
```
This command actually creates a temporary testing user, logs it in, then creates two things and two channles on behalf of this user.
This way we have Mainflux system that have been quickly provisioned with one simple testing scenario.
You can read more about system provisioning in a dedicated [Provisioning](./provisioning.md) chapter
Output of the command is something like this:
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjMzODg0NzcsImlhdCI6MTUyMzM1MjQ3NywiaXNzIjoibWFpbmZsdXgiLCJzdWIiOiJqb2huLmRvZUBlbWFpbC5jb20ifQ.cygz9zoqD7Rd8f88hpQNilTCAS1DrLLgLg4PRcH-iAI"
"email": "friendly_beaver@email.com",
"password": "123"
}
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDcwMjE3ODAsImlhdCI6MTU0Njk4NTc4MCwiaXNzIjoibWFpbmZsdXgiLCJzdWIiOiJmcmllbmRseV9iZWF2ZXJAZW1haWwuY29tIn0.Tyk31Ae680KqMrDqP895PRZg_GUytLE0IMIR_o3oO7o"
[
{
"id": "513d02d2-16c1-4f23-98be-9e12f8fee898",
"key": "69590b3a-9d76-4baa-adae-9b5fec0ea14f",
"name": "d0",
"type": "device"
},
{
"id": "bf78ca98-2fef-4cfc-9f26-e02da5ecdf67",
"key": "840c1ea1-2e8d-4809-a6d3-3433a5c489d2",
"name": "d1",
"type": "app"
}
]
[
{
"id": "b7bfc4b6-c18d-47c5-b343-98235c5acc19",
"name": "c0"
},
{
"id": "378678cd-891b-4a39-b026-869938783f54",
"name": "c1"
}
]
```
## System provisioning
In the Mainflux system terminal (where docker composition is running) you should see following logs:
```bash
mainflux-users | {"level":"info","message":"Method register for user friendly_beaver@email.com took 97.573974ms to complete without errors.","ts":"2019-01-08T22:16:20.745989495Z"}
mainflux-users | {"level":"info","message":"Method login for user friendly_beaver@email.com took 69.308406ms to complete without errors.","ts":"2019-01-08T22:16:20.820610461Z"}
mainflux-users | {"level":"info","message":"Method identity for client friendly_beaver@email.com took 50.903µs to complete without errors.","ts":"2019-01-08T22:16:20.822208948Z"}
mainflux-things | {"level":"info","message":"Method add_thing for key eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDcwMjE3ODAsImlhdCI6MTU0Njk4NTc4MCwiaXNzIjoibWFpbmZsdXgiLCJzdWIiOiJmcmllbmRseV9iZWF2ZXJAZW1haWwuY29tIn0.Tyk31Ae680KqMrDqP895PRZg_GUytLE0IMIR_o3oO7o and thing 513d02d2-16c1-4f23-98be-9e12f8fee898 took 4.865299ms to complete without errors.","ts":"2019-01-08T22:16:20.826786175Z"}
Before proceeding, make sure that you have created a new account, and obtained
an authorization key.
### Provisioning devices
Devices are provisioned by executing request `POST /clients`, with a
`"type":"device"` specified in JSON payload. Note that you will also need
`user_auth_token` in order to provision clients (both devices and application)
that belong to this particular user.
...
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json; charset=utf-8" -H "Authorization: <user_auth_token>" https://localhost/clients -d '{"type":"device", "name":"weio"}'
This proves that these provisioning commands were sent from the CLI to the Mainflux system.
## Step 4 - Send Messages
Once system is provisioned, `thing` can start sending messages on a `channel`:
```bash
mainflux-cli messages send <channel_id> '[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]' <thing_key>
```
Response will contain `Location` header whose value represents path to newly
created client:
```
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /clients/81380742-7116-4f6f-9800-14fe464f6773
Date: Tue, 10 Apr 2018 10:02:59 GMT
Content-Length: 0
For example:
```bash
mainflux-cli messages send b7bfc4b6-c18d-47c5-b343-98235c5acc19 '[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]' 69590b3a-9d76-4baa-adae-9b5fec0ea14f
```
### Provisioning applications
In the Mainflux system terminal you should see following logs:
Applications are provisioned by executing HTTP request `POST /clients`, with
`"type":"app"` specified in JSON payload.
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json; charset=utf-8" -H "Authorization: <user_auth_token>" https://localhost/clients -d '{"type":"app", "name":"myapp"}'
```bash
mainflux-things | {"level":"info","message":"Method can_access for channel b7bfc4b6-c18d-47c5-b343-98235c5acc19 and thing 513d02d2-16c1-4f23-98be-9e12f8fee898 took 1.410194ms to complete without errors.","ts":"2019-01-08T22:19:30.148097648Z"}
mainflux-http | {"level":"info","message":"Method publish took 336.685µs to complete without errors.","ts":"2019-01-08T22:19:30.148689601Z"}
mainflux-normalizer | {"level":"info","message":"Method normalize took 108.126µs to complete without errors.","ts":"2019-01-08T22:19:30.149500543Z"}
```
Response will contain `Location` header whose value represents path to newly
created client (same as for devices):
```
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /clients/cb63f852-2d48-44f0-a0cf-e450496c6c92
Date: Tue, 10 Apr 2018 10:33:17 GMT
Content-Length: 0
```
### Retrieving provisioned clients
In order to retrieve data of provisioned clients that is written in database, you
can send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/clients
```
Notice that you will receive only those clients that were provisioned by
`user_auth_token` owner.
```
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 10 Apr 2018 10:50:12 GMT
Content-Length: 1105
{
"clients": [
{
"id": "81380742-7116-4f6f-9800-14fe464f6773",
"type": "device",
"name": "weio",
"key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjMzNTQ1NzksImlzcyI6Im1haW5mbHV4Iiwic3ViIjoiODEzODA3NDItNzExNi00ZjZmLTk4MDAtMTRmZTQ2NGY2NzczIn0.5s8s1hlK-l30kQAyHxEZO_M2NIQw53MQuy7b3Wf3OOE"
},
{
"id": "cb63f852-2d48-44f0-a0cf-e450496c6c92",
"type": "app",
"name": "myapp",
"key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjMzNTYzOTcsImlzcyI6Im1haW5mbHV4Iiwic3ViIjoiY2I2M2Y4NTItMmQ0OC00NGYwLWEwY2YtZTQ1MDQ5NmM2YzkyIn0.FE6DWB3yJmBb8uojpQJaKUEbD0Elrjx0HhJA28bVzkU"
}
]
}
```
You can specify `offset` and `limit` parameters in order to fetch specific
group of clients. In that case, your request should look like:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/clients?offset=0&limit=5
```
If you don't provide them, default values will be used instead: 0 for `offset`,
and 10 for `limit`. Note that `limit` cannot be set to values greater than 100. Providing
invalid values will be considered malformed request.
### Removing clients
In order to remove you own client you can send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/clients/<client_id>
```
### Provisioning channels
Channels are provisioned by executing request `POST /channels`:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/json; charset=utf-8" -H "Authorization: <user_auth_token>" https://localhost/channels -d '{"name":"mychan"}'
```
After sending request you should receive response with `Location` header that
contains path to newly created channel:
```
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /channels/19daa7a8-a489-4571-8714-ef1a214ed914
Date: Tue, 10 Apr 2018 11:30:07 GMT
Content-Length: 0
```
### Retrieving provisioned channels
To retreve provisioned channels you should send request to `/channels` with
authorization token in `Authorization` header:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/channels
```
Note that you will receive only those channels that were created by authorization
token's owner.
```
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 10 Apr 2018 11:38:06 GMT
Content-Length: 139
{
"channels": [
{
"id": "19daa7a8-a489-4571-8714-ef1a214ed914",
"name": "mychan"
}
]
}
```
You can specify `offset` and `limit` parameters in order to fetch specific
group of channels. In that case, your request should look like:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/channels?offset=0&limit=5
```
If you don't provide them, default values will be used instead: 0 for `offset`,
and 10 for `limit`. Note that `limit` cannot be set to values greater than 100. Providing
invalid values will be considered malformed request.
### Removing channels
In order to remove specific channel you should send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>
```
## Access control
Channel can be observed as a communication group of clients. Only clients that
are connected to the channel can send and receive messages from other clients
in this channel. Clients that are not connected to this channel are not allowed
to communicate over it.
Only user, who is the owner of a channel and of the clients, can connect the
clients to the channel (which is equivalent of giving permissions to these clients
to communicate over given communication group).
To connect client to the channel you should send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X PUT -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/clients/<client_id>
```
You can observe which clients are connected to specific channel:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>
```
You should receive response with the lists of connected clients in `connected` field
similar to this one:
```
{
"id": "19daa7a8-a489-4571-8714-ef1a214ed914",
"name": "mychan",
"connected": [
{
"id": "81380742-7116-4f6f-9800-14fe464f6773",
"type": "device",
"name": "weio",
"key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjMzNTQ1NzksImlzcyI6Im1haW5mbHV4Iiwic3ViIjoiODEzODA3NDItNzExNi00ZjZmLTk4MDAtMTRmZTQ2NGY2NzczIn0.5s8s1hlK-l30kQAyHxEZO_M2NIQw53MQuy7b3Wf3OOE"
}
]
}
```
If you want to disconnect your device from the channel, send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X DELETE -H "Authorization: <user_auth_token>" https://localhost/channels/<channel_id>/clients/<client_id>
```
## Sending messages
Once a channel is provisioned and client is connected to it, it can start to
publish messages on the channel. The following sections will provide an example
of message publishing for each of the supported protocols.
### HTTP
To publish message over channel, client should send following request:
```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -X POST -H "Content-Type: application/senml+json" -H "Authorization: <client_token>" https://localhost/channels/<channel_id>/messages -d '[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]'
```
Note that you should always send array of messages in senML format.
### WebSocket
To publish and receive messages over channel using web socket, you should first
send handshake request to `/channels/<channel_id>/messages` path. Don't forget
to send `Authorization` header with client authorization token.
If you are not able to send custom headers in your handshake request, send it as
query parameter `authorization`. Then your path should look like this
`/channels/<channel_id>/messages?authorization=<client_auth_key>`.
This proves that messages have been well send through the system, via protocol adapter (`mainflux-http`) and `normalizer` service which corectly parsed messages.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Some files were not shown because too many files have changed in this diff Show More