Compare commits

..

77 Commits

Author SHA1 Message Date
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
844 changed files with 106810 additions and 8292 deletions
+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 -1
View File
@@ -1,4 +1,4 @@
build
*.pb.go
site/
Generated
+165 -11
View File
@@ -34,6 +34,12 @@
packages = ["quantile"]
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
branch = "master"
name = "github.com/buger/jsonparser"
packages = ["."]
revision = "2cac668e8456b4284edb0715e17e2af02d3ec993"
[[projects]]
name = "github.com/cenkalti/backoff"
packages = ["."]
@@ -58,12 +64,6 @@
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/dereulenspiegel/coap-mux"
packages = ["."]
revision = "e137e310a99955be01d60b2237da89153ea40987"
[[projects]]
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
@@ -83,10 +83,10 @@
version = "v0.3.3"
[[projects]]
branch = "master"
name = "github.com/dustin/go-coap"
name = "github.com/fatih/color"
packages = ["."]
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
name = "github.com/go-kit/kit"
@@ -108,6 +108,21 @@
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
version = "v0.3.0"
[[projects]]
name = "github.com/go-redis/redis"
packages = [
".",
"internal",
"internal/consistenthash",
"internal/hashtag",
"internal/pool",
"internal/proto",
"internal/singleflight",
"internal/util"
]
revision = "f3bba01df2026fc865f7782948845db9cf44cf23"
version = "v6.14.1"
[[projects]]
name = "github.com/go-stack/stack"
packages = ["."]
@@ -120,6 +135,17 @@
revision = "fd0aebc74e908868b09ac140fb5a53cb363884c1"
version = "1.2"
[[projects]]
branch = "master"
name = "github.com/gocql/gocql"
packages = [
".",
"internal/lru",
"internal/murmur",
"internal/streams"
]
revision = "651d6b1f343c6e168e45d119dcfe8f71e5bafb32"
[[projects]]
name = "github.com/gogo/protobuf"
packages = ["proto"]
@@ -138,12 +164,45 @@
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/golang/snappy"
packages = ["."]
revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a"
[[projects]]
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
name = "github.com/hailocab/go-hostpool"
packages = ["."]
revision = "e80d13ce29ede4452c43dea11e79b9bc8a15b478"
[[projects]]
branch = "master"
name = "github.com/hokaccha/go-prettyjson"
packages = ["."]
revision = "e6b9231a2b1cc5798b336fdbf99c16cd8ad1e0c9"
[[projects]]
name = "github.com/inconshreveable/mousetrap"
packages = ["."]
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0"
[[projects]]
name = "github.com/influxdata/influxdb"
packages = [
"client/v2",
"models",
"pkg/escape"
]
revision = "02d7d4f043b34ecb4e9b2dbec298c6f9450c2a32"
version = "v1.5.2"
[[projects]]
branch = "master"
name = "github.com/kr/logfmt"
@@ -159,12 +218,79 @@
]
revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
[[projects]]
name = "github.com/mattn/go-colorable"
packages = ["."]
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
name = "github.com/mongodb/mongo-go-driver"
packages = [
"bson",
"bson/builder",
"bson/decimal",
"bson/elements",
"bson/objectid",
"bson/parser",
"bson/parser/ast",
"core/address",
"core/auth",
"core/auth/internal/gssapi",
"core/command",
"core/compressor",
"core/connection",
"core/connstring",
"core/description",
"core/dispatch",
"core/event",
"core/option",
"core/readconcern",
"core/readpref",
"core/result",
"core/session",
"core/tag",
"core/topology",
"core/uuid",
"core/version",
"core/wiremessage",
"core/writeconcern",
"internal",
"mongo",
"mongo/aggregateopt",
"mongo/changestreamopt",
"mongo/clientopt",
"mongo/collectionopt",
"mongo/countopt",
"mongo/dbopt",
"mongo/deleteopt",
"mongo/distinctopt",
"mongo/dropcollopt",
"mongo/findopt",
"mongo/indexopt",
"mongo/insertopt",
"mongo/listcollectionopt",
"mongo/listdbopt",
"mongo/mongoopt",
"mongo/replaceopt",
"mongo/runcmdopt",
"mongo/updateopt"
]
revision = "8df56f107cdd485351eb2523992c3364114b4b2e"
version = "v0.0.10"
[[projects]]
name = "github.com/nats-io/go-nats"
packages = [
@@ -311,9 +437,24 @@
revision = "e9556a45379ef1da12e54847edb2fb3d7d566f36"
version = "0.3.0"
[[projects]]
name = "github.com/spf13/cobra"
packages = ["."]
revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385"
version = "v0.0.3"
[[projects]]
name = "github.com/spf13/pflag"
packages = ["."]
revision = "583c0c0531f06d5278b7d917446061adc344b5cd"
version = "v1.0.1"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
packages = [
"assert",
"require"
]
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
@@ -329,6 +470,7 @@
packages = [
"bcrypt",
"blowfish",
"pbkdf2",
"ssh/terminal"
]
revision = "2d027ae1dddd4694d54f7a8b6cbe78dca8720226"
@@ -347,6 +489,12 @@
]
revision = "f73e4c9ed3b7ebdd5f699a16a880c2b1994e50dd"
[[projects]]
branch = "master"
name = "golang.org/x/sync"
packages = ["semaphore"]
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
@@ -420,6 +568,12 @@
revision = "c87af80f3cc5036b55b83d77171e156791085e2e"
version = "v1.7.1"
[[projects]]
name = "gopkg.in/inf.v0"
packages = ["."]
revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf"
version = "v0.9.1"
[[projects]]
name = "gopkg.in/ory-am/dockertest.v3"
packages = ["."]
@@ -429,6 +583,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "cce4c6cd08b6213615afc135d8e7d4f255d4ee32b42062e1f3208de246366418"
inputs-digest = "1f18adf44738d8f5c48b2f0576ab90c2f2c549f13a846b8f3c95aabaebe8efb4"
solver-name = "gps-cdcl"
solver-version = 1
+20 -8
View File
@@ -6,17 +6,13 @@
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/fatih/color"
version = "1.7.0"
[[constraint]]
name = "github.com/go-kit/kit"
@@ -31,8 +27,8 @@
version = "1.1.0"
[[constraint]]
name = "github.com/jinzhu/gorm"
version = "1.9.1"
branch = "master"
name = "github.com/hokaccha/go-prettyjson"
[[constraint]]
branch = "master"
@@ -54,6 +50,10 @@
name = "github.com/sony/gobreaker"
version = "0.3.0"
[[constraint]]
name = "github.com/spf13/cobra"
version = "0.0.3"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
@@ -74,6 +74,18 @@
branch = "master"
name = "github.com/rubenv/sql-migrate"
[[constraint]]
name = "github.com/influxdata/influxdb"
version = "1.5.2"
[[constraint]]
name = "github.com/mongodb/mongo-go-driver"
version = "0.0.5"
[[constraint]]
branch = "master"
name = "github.com/gocql/gocql"
[prune]
go-tests = true
unused-packages = true
+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"
+39 -14
View File
@@ -1,6 +1,7 @@
BUILD_DIR = build
SERVICES = users clients http normalizer ws coap
SERVICES = users things http normalizer ws influxdb-writer influxdb-reader mongodb-writer mongodb-reader cassandra-writer cassandra-reader cli
DOCKERS = $(addprefix docker_,$(SERVICES))
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
CGO_ENABLED ?= 0
GOOS ?= linux
@@ -9,23 +10,31 @@ 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
clean:
rm -rf ${BUILD_DIR}
rm -rf mqtt/node_modules
install:
cp ${BUILD_DIR}/* $(GOBIN)
test:
GOCACHE=off go test -v -race -tags test $(shell go list ./... | grep -v 'vendor\|cmd')
proto:
protoc --go_out=plugins=grpc:. *.proto
$(SERVICES): proto
$(SERVICES):
$(call compile_service,$(@))
$(DOCKERS):
@@ -33,14 +42,26 @@ $(DOCKERS):
dockers: $(DOCKERS)
docker build --tag=mainflux/dashflux -f dashflux/docker/Dockerfile dashflux
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile mqtt
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile .
$(DOCKERS_DEV):
$(call make_docker_dev,$(@))
dockers_dev: $(DOCKERS_DEV)
mqtt:
cd mqtt && npm install
define docker_push
for svc in $(SERVICES); do \
docker push mainflux/$$svc:$(1); \
done
docker push mainflux/dashflux:$(1)
docker push mainflux/mqtt:$(1)
endef
latest: dockers
for svc in $(SERVICES); do \
docker push mainflux/$$svc; \
done
docker push mainflux/dashflux
docker push mainflux/mqtt
$(call docker_push,latest)
release:
$(eval version = $(shell git describe --abbrev=0 --tags))
@@ -48,9 +69,13 @@ release:
$(MAKE) dockers
for svc in $(SERVICES); do \
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
docker push mainflux/$$svc:$(version); \
done
docker tag mainflux/dashflux mainflux/dashflux:$(version)
docker push mainflux/dashflux:$(version)
docker tag mainflux/mqtt mainflux/mqtt:$(version)
docker push mainflux/mqtt:$(version)
$(call docker_push,$(version))
rundev:
cd scripts && ./run.sh
run:
docker-compose -f docker/docker-compose.yml up
+7
View File
@@ -1,3 +1,10 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package mainflux
// Response contains HTTP response specific methods.
+87
View File
@@ -0,0 +1,87 @@
# Mainflux CLI
## Build
From the project root:
```
make cli
```
## Usage
### Service
#### Get the service verison
```
mainflux-cli version
```
### User 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
#### Provision Device
```
mainflux-cli things create '{"type":"device", "name":"nyDevice"}' <user_auth_token>
```
#### Provision Application
```
mainflux-cli things create '{"type":"app", "name":"nyDevice"}' <user_auth_token>
```
#### Retrieve All Things
```
mainflux-cli things get all --offset=1 --limit=5 <user_auth_token>
```
#### Retrieve Thing By ID
```
mainflux-cli things get <thing_id> --offset=1 --limit=5 <user_auth_token>
```
#### Remove Thing
```
mainflux-cli things delete <thing_id> <user_auth_token>
```
#### Provision Channel
```
mainflux-cli channels create '{"name":"nyChannel"}' <user_auth_token>
```
#### Retrieve All Channels
```
mainflux-cli channels get all --offset=1 --limit=5 <user_auth_token>
```
#### Retrievie Channel By ID
```
mainflux-cli channels get <channel_id> --offset=1 --limit=5 <user_auth_token>
```
#### Remove Channel
```
mainflux-cli channels delete <channel_id> <user_auth_token>
```
### Access control
#### Connect Thing to a Channel
```
mainflux-cli things connect <thing_id> <channel_id> <user_auth_token>
```
#### Disconnect Things from a Channel
```
mainflux-cli things disconnect <thing_id> <channel_id> <user_auth_token>
```
### Messaging
#### Send a message over HTTP
```
mainflux-cli msg 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_auth_token>
```
+133
View File
@@ -0,0 +1,133 @@
//
// 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
}
flush(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
}
flush(l)
return
}
c, err := sdk.Channel(args[0], args[1])
if err != nil {
logError(err)
return
}
flush(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) != 3 {
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()
},
},
}
// NewChannelsCmd returns channels command.
func NewChannelsCmd() *cobra.Command {
cmd := cobra.Command{
Use: "channels",
Short: "Manipulation with channels",
Long: `Manipulation with channels: create, delete or update channels`,
Run: func(cmd *cobra.Command, args []string) {
logUsage(cmd.Short)
},
}
for i := range cmdChannels {
cmd.AddCommand(&cmdChannels[i])
}
return &cmd
}
+48
View File
@@ -0,0 +1,48 @@
//
// 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> <JSON_string> <thing_token>",
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()
},
},
}
// NewMessagesCmd returns messages command.
func NewMessagesCmd() *cobra.Command {
cmd := cobra.Command{
Use: "msg",
Short: "Send or retrieve messages",
Long: `Send or retrieve messages: control message flow on the channel`,
}
for i := range cmdMessages {
cmd.AddCommand(&cmdMessages[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
}
+170
View File
@@ -0,0 +1,170 @@
//
// 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
}
flush(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
}
flush(l)
return
}
t, err := sdk.Thing(args[0], args[1])
if err != nil {
logError(err)
return
}
flush(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) != 3 {
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()
},
},
}
// NewThingsCmd returns things command.
func NewThingsCmd() *cobra.Command {
cmd := cobra.Command{
Use: "things",
Short: "things <options>",
Long: `Things handling: create, delete or update things`,
Run: func(cmd *cobra.Command, args []string) {
logUsage(cmd.Short)
},
}
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
}
flush(token)
},
},
}
// NewUsersCmd returns users command.
func NewUsersCmd() *cobra.Command {
cmd := cobra.Command{
Use: "users",
Short: "users create/token <email> <password>",
Long: `Manages users in the system (create account or token)`,
Run: func(cmd *cobra.Command, args []string) {
logUsage(cmd.Short)
},
}
for i := range cmdUsers {
cmd.AddCommand(&cmdUsers[i])
}
return &cmd
}
+38
View File
@@ -0,0 +1,38 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package cli
import (
"fmt"
"github.com/davecgh/go-spew/spew"
"github.com/fatih/color"
)
var (
// Limit query parameter
Limit uint = 10
// Offset query parameter
Offset uint
)
func flush(i interface{}) {
fmt.Printf("%s", color.BlueString(spew.Sdump(i)))
}
func logUsage(u string) {
fmt.Printf(color.YellowString("Usage: %s\n"), u)
}
func logError(err error) {
fmt.Printf("%s\n", color.RedString(err.Error()))
}
func logOK() {
fmt.Printf("%s\n", color.GreenString("OK"))
}
+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: "Get version of Mainflux Things Service",
Long: `Mainflux server health check`,
Run: func(cmd *cobra.Command, args []string) {
v, err := sdk.Version()
if err != nil {
logError(err)
return
}
flush(v)
},
}
}
-81
View File
@@ -1,81 +0,0 @@
# Clients
Clients service provides an HTTP API for managing platform resources: devices,
applications and channels. Through this API clients are able to do the following
actions:
- provision new clients (i.e. devices & applications)
- create new channels
- "connect" clients into the channels
For in-depth explanation of the aforementioned scenarios, as well as thorough
understanding of Mainflux, please check out the [official documentation][doc].
## 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_CLIENTS_DB_HOST | Database host address | localhost |
| MF_CLIENTS_DB_PORT | Database host port | 5432 |
| MF_CLIENTS_DB_USER | Database user | mainflux |
| MF_CLIENTS_DB_PASSWORD | Database password | mainflux |
| MF_CLIENTS_DB | Name of the database used by the service | clients |
| MF_CLIENTS_HTTP_PORT | Clients service HTTP port | 8180 |
| MF_CLIENTS_GRPC_PORT | Clients service gRPC port | 8181 |
| MF_USERS_URL | Users service URL | localhost:8181 |
| MF_CLIENTS_SECRET | String used for signing tokens | clients |
## 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"
services:
clients:
image: mainflux/clients:[version]
container_name: [instance name]
ports:
- [host machine port]:[configured HTTP port]
environment:
MF_CLIENTS_DB_HOST: [Database host address]
MF_CLIENTS_DB_PORT: [Database host port]
MF_CLIENTS_DB_USER: [Database user]
MF_CLIENTS_DB_PASS: [Database password]
MF_CLIENTS_DB: [Name of the database used by the service]
MF_CLIENTS_HTTP_PORT: [Service HTTP port]
MF_CLIENTS_GRPC_PORT: [Service gRPC port]
MF_USERS_URL: [Users service URL]
MF_CLIENTS_SECRET: [String used for signing tokens]
```
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 clients
make clients
# copy binary to bin
make install
# set the environment variables and run the service
MF_CLIENTS_DB_HOST=[Database host address] MF_CLIENTS_DB_PORT=[Database host port] MF_CLIENTS_DB_USER=[Database user] MF_CLIENTS_DB_PASS=[Database password] MF_CLIENTS_DB=[Name of the database used by the service] MF_CLIENTS_HTTP_PORT=[Service HTTP port] MF_CLIENTS_GRPC_PORT=[Service gRPC port] MF_USERS_URL=[Users service URL] MF_CLIENTS_SECRET=[String used for signing tokens] $GOBIN/mainflux-clients
```
## Usage
For more information about service capabilities and its usage, please check out
the [API documentation](swagger.yaml).
[doc]: http://mainflux.readthedocs.io
-49
View File
@@ -1,49 +0,0 @@
package grpc
import (
"github.com/go-kit/kit/endpoint"
kitgrpc "github.com/go-kit/kit/transport/grpc"
"github.com/mainflux/mainflux"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
var _ mainflux.ClientsServiceClient = (*grpcClient)(nil)
type grpcClient struct {
canAccess endpoint.Endpoint
}
// NewClient returns new gRPC client instance.
func NewClient(conn *grpc.ClientConn) mainflux.ClientsServiceClient {
endpoint := kitgrpc.NewClient(
conn,
"mainflux.ClientsService",
"CanAccess",
encodeCanAccessRequest,
decodeCanAccessResponse,
mainflux.Identity{},
).Endpoint()
return &grpcClient{endpoint}
}
func (client grpcClient) CanAccess(ctx context.Context, req *mainflux.AccessReq, _ ...grpc.CallOption) (*mainflux.Identity, error) {
res, err := client.canAccess(ctx, accessReq{req.GetToken(), req.GetChanID()})
if err != nil {
return nil, err
}
ar := res.(accessRes)
return &mainflux.Identity{Value: ar.id}, ar.err
}
func encodeCanAccessRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(accessReq)
return &mainflux.AccessReq{Token: req.clientKey, ChanID: req.chanID}, nil
}
func decodeCanAccessResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(*mainflux.Identity)
return accessRes{res.GetValue(), nil}, nil
}
-2
View File
@@ -1,2 +0,0 @@
// Package grpc contains implementation of clients service gRPC API.
package grpc
-22
View File
@@ -1,22 +0,0 @@
package grpc
import (
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/clients"
context "golang.org/x/net/context"
)
func canAccessEndpoint(svc clients.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(accessReq)
if err := req.validate(); err != nil {
return nil, err
}
id, err := svc.CanAccess(req.clientKey, req.chanID)
if err != nil {
return accessRes{"", err}, err
}
return accessRes{id, nil}, nil
}
}
-87
View File
@@ -1,87 +0,0 @@
package grpc_test
import (
"context"
"fmt"
"net"
"testing"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/clients"
grpcapi "github.com/mainflux/mainflux/clients/api/grpc"
"github.com/mainflux/mainflux/clients/mocks"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
port = 8080
token = "token"
wrong = "wrong"
email = "john.doe@email.com"
)
var (
client = clients.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
channel = clients.Channel{Name: "test"}
)
func newService(tokens map[string]string) clients.Service {
users := mocks.NewUsersService(tokens)
clientsRepo := mocks.NewClientRepository()
channelsRepo := mocks.NewChannelRepository(clientsRepo)
hasher := mocks.NewHasher()
idp := mocks.NewIdentityProvider()
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
}
func startGRPCServer(svc clients.Service, port int) {
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
server := grpc.NewServer()
mainflux.RegisterClientsServiceServer(server, grpcapi.NewServer(svc))
go server.Serve(listener)
}
func TestCanAccess(t *testing.T) {
svc := newService(map[string]string{token: email})
startGRPCServer(svc, port)
connectedClientID, _ := svc.AddClient(token, client)
connectedClient, _ := svc.ViewClient(token, connectedClientID)
clientID, _ := svc.AddClient(token, client)
client, _ := svc.ViewClient(token, clientID)
chanID, _ := svc.CreateChannel(token, channel)
svc.Connect(token, chanID, connectedClientID)
usersAddr := fmt.Sprintf("localhost:%d", port)
conn, _ := grpc.Dial(usersAddr, grpc.WithInsecure())
cli := grpcapi.NewClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
cases := map[string]struct {
clientKey string
chanID string
id string
code codes.Code
}{
"check if connected client can access existing channel": {connectedClient.Key, chanID, connectedClientID, codes.OK},
"check if unconnected client can access existing channel": {client.Key, chanID, "", codes.PermissionDenied},
"check if wrong client can access existing channel": {wrong, chanID, "", codes.PermissionDenied},
"check if connected client can access non-existent channel": {connectedClient.Key, "1", "", codes.InvalidArgument},
}
for desc, tc := range cases {
id, err := cli.CanAccess(ctx, &mainflux.AccessReq{tc.clientKey, tc.chanID})
e, ok := status.FromError(err)
assert.True(t, ok, "OK expected to be true")
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", desc, tc.id, id.GetValue()))
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", desc, tc.code, e.Code()))
}
}
-18
View File
@@ -1,18 +0,0 @@
package grpc
import (
"github.com/asaskevich/govalidator"
"github.com/mainflux/mainflux/clients"
)
type accessReq struct {
clientKey string
chanID string
}
func (req accessReq) validate() error {
if !govalidator.IsUUID(req.chanID) || req.clientKey == "" {
return clients.ErrMalformedEntity
}
return nil
}
-6
View File
@@ -1,6 +0,0 @@
package grpc
type accessRes struct {
id string
err error
}
-59
View File
@@ -1,59 +0,0 @@
package grpc
import (
kitgrpc "github.com/go-kit/kit/transport/grpc"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/clients"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var _ mainflux.ClientsServiceServer = (*grpcServer)(nil)
type grpcServer struct {
handler kitgrpc.Handler
}
// NewServer returns new ClientsServiceServer instance.
func NewServer(svc clients.Service) mainflux.ClientsServiceServer {
handler := kitgrpc.NewServer(
canAccessEndpoint(svc),
decodeCanAccessRequest,
encodeCanAccessResponse,
)
return &grpcServer{handler}
}
func (s *grpcServer) CanAccess(ctx context.Context, req *mainflux.AccessReq) (*mainflux.Identity, error) {
_, res, err := s.handler.ServeGRPC(ctx, req)
if err != nil {
return nil, encodeError(err)
}
return res.(*mainflux.Identity), nil
}
func decodeCanAccessRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*mainflux.AccessReq)
return accessReq{req.GetToken(), req.GetChanID()}, nil
}
func encodeCanAccessResponse(_ context.Context, grpcRes interface{}) (interface{}, error) {
res := grpcRes.(accessRes)
return &mainflux.Identity{Value: res.id}, encodeError(res.err)
}
func encodeError(err error) error {
if err == nil {
return nil
}
switch err {
case clients.ErrMalformedEntity:
return status.Error(codes.InvalidArgument, "received invalid can access request")
case clients.ErrUnauthorizedAccess:
return status.Error(codes.PermissionDenied, "missing or invalid credentials provided")
default:
return status.Error(codes.Internal, "internal server error")
}
}
-2
View File
@@ -1,2 +0,0 @@
// Package http contains implementation of clients service HTTP API.
package http
-217
View File
@@ -1,217 +0,0 @@
package http
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/mainflux/mainflux/clients"
)
func addClientEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(addClientReq)
if err := req.validate(); err != nil {
return nil, err
}
id, err := svc.AddClient(req.key, req.client)
if err != nil {
return nil, err
}
return clientRes{id: id, created: true}, nil
}
}
func updateClientEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateClientReq)
if err := req.validate(); err != nil {
return nil, err
}
req.client.ID = req.id
if err := svc.UpdateClient(req.key, req.client); err != nil {
return nil, err
}
return clientRes{id: req.id, created: false}, nil
}
}
func viewClientEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
if err := req.validate(); err != nil {
return nil, err
}
client, err := svc.ViewClient(req.key, req.id)
if err != nil {
return nil, err
}
return viewClientRes{client}, nil
}
}
func listClientsEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(listResourcesReq)
if err := req.validate(); err != nil {
return nil, err
}
clients, err := svc.ListClients(req.key, req.offset, req.limit)
if err != nil {
return nil, err
}
return listClientsRes{clients}, nil
}
}
func removeClientEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
err := req.validate()
if err == clients.ErrNotFound {
return removeRes{}, nil
}
if err != nil {
return nil, err
}
if err = svc.RemoveClient(req.key, req.id); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func createChannelEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(createChannelReq)
if err := req.validate(); err != nil {
return nil, err
}
id, err := svc.CreateChannel(req.key, req.channel)
if err != nil {
return nil, err
}
return channelRes{id: id, created: true}, nil
}
}
func updateChannelEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(updateChannelReq)
if err := req.validate(); err != nil {
return nil, err
}
req.channel.ID = req.id
if err := svc.UpdateChannel(req.key, req.channel); err != nil {
return nil, err
}
return channelRes{id: req.id, created: false}, nil
}
}
func viewChannelEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
if err := req.validate(); err != nil {
return nil, err
}
channel, err := svc.ViewChannel(req.key, req.id)
if err != nil {
return nil, err
}
return viewChannelRes{channel}, nil
}
}
func listChannelsEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(listResourcesReq)
if err := req.validate(); err != nil {
return nil, err
}
channels, err := svc.ListChannels(req.key, req.offset, req.limit)
if err != nil {
return nil, err
}
return listChannelsRes{channels}, nil
}
}
func removeChannelEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
req := request.(viewResourceReq)
if err := req.validate(); err != nil {
if err == clients.ErrNotFound {
return removeRes{}, nil
}
return nil, err
}
if err := svc.RemoveChannel(req.key, req.id); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func connectEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
cr := request.(connectionReq)
if err := cr.validate(); err != nil {
return nil, err
}
if err := svc.Connect(cr.key, cr.chanID, cr.clientID); err != nil {
return nil, err
}
return connectionRes{}, nil
}
}
func disconnectEndpoint(svc clients.Service) endpoint.Endpoint {
return func(_ context.Context, request interface{}) (interface{}, error) {
cr := request.(connectionReq)
if err := cr.validate(); err != nil {
return nil, err
}
if err := svc.Disconnect(cr.key, cr.chanID, cr.clientID); err != nil {
return nil, err
}
return disconnectionRes{}, nil
}
}
-610
View File
@@ -1,610 +0,0 @@
package http_test
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/mainflux/mainflux/clients"
httpapi "github.com/mainflux/mainflux/clients/api/http"
"github.com/mainflux/mainflux/clients/mocks"
"github.com/stretchr/testify/assert"
)
const (
contentType = "application/json"
invalidEmail = "userexample.com"
email = "user@example.com"
token = "token"
invalidToken = "invalid_token"
wrongID = "123e4567-e89b-12d3-a456-000000000042"
id = "123e4567-e89b-12d3-a456-000000000001"
)
var (
client = clients.Client{Type: "app", Name: "test_app", Payload: "test_payload"}
channel = clients.Channel{Name: "test"}
)
type testRequest struct {
client *http.Client
method string
url string
contentType string
token string
body io.Reader
}
func (tr testRequest) make() (*http.Response, error) {
req, err := http.NewRequest(tr.method, tr.url, tr.body)
if err != nil {
return nil, err
}
if tr.token != "" {
req.Header.Set("Authorization", tr.token)
}
if tr.contentType != "" {
req.Header.Set("Content-Type", tr.contentType)
}
return tr.client.Do(req)
}
func newService(tokens map[string]string) clients.Service {
users := mocks.NewUsersService(tokens)
clientsRepo := mocks.NewClientRepository()
channelsRepo := mocks.NewChannelRepository(clientsRepo)
hasher := mocks.NewHasher()
idp := mocks.NewIdentityProvider()
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
}
func newServer(svc clients.Service) *httptest.Server {
mux := httpapi.MakeHandler(svc)
return httptest.NewServer(mux)
}
func toJSON(data interface{}) string {
jsonData, _ := json.Marshal(data)
return string(jsonData)
}
func TestAddClient(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
data := toJSON(client)
invalidData := toJSON(clients.Client{
Type: "foo",
Name: "invalid_client",
Payload: "some_payload",
})
cases := []struct {
desc string
req string
contentType string
auth string
status int
location string
}{
{"add valid client", data, contentType, token, http.StatusCreated, fmt.Sprintf("/clients/%s", id)},
{"add client with invalid data", invalidData, contentType, token, http.StatusBadRequest, ""},
{"add client with invalid auth token", data, contentType, invalidToken, http.StatusForbidden, ""},
{"add client with invalid request format", "}", contentType, token, http.StatusBadRequest, ""},
{"add client with empty JSON request", "{}", contentType, token, http.StatusBadRequest, ""},
{"add client with empty request", "", contentType, token, http.StatusBadRequest, ""},
{"add client with missing content type", data, "", token, http.StatusUnsupportedMediaType, ""},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodPost,
url: fmt.Sprintf("%s/clients", ts.URL),
contentType: tc.contentType,
token: tc.auth,
body: strings.NewReader(tc.req),
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
location := res.Header.Get("Location")
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
}
}
func TestUpdateClient(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
data := toJSON(client)
invalidData := toJSON(clients.Client{
Type: "foo",
Name: client.Name,
Payload: client.Payload,
})
id, _ := svc.AddClient(token, client)
cases := []struct {
desc string
req string
id string
contentType string
auth string
status int
}{
{"update existing client", data, id, contentType, token, http.StatusOK},
{"update non-existent client", data, wrongID, contentType, token, http.StatusNotFound},
{"update client with invalid id", data, "1", contentType, token, http.StatusNotFound},
{"update client with invalid data", invalidData, id, contentType, token, http.StatusBadRequest},
{"update client with invalid user token", data, id, contentType, invalidToken, http.StatusForbidden},
{"update client with invalid data format", "{", id, contentType, token, http.StatusBadRequest},
{"update client with empty JSON request", "{}", id, contentType, token, http.StatusBadRequest},
{"update client with empty request", "", id, contentType, token, http.StatusBadRequest},
{"update client with missing content type", data, id, "", token, http.StatusUnsupportedMediaType},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodPut,
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
contentType: tc.contentType,
token: tc.auth,
body: strings.NewReader(tc.req),
}
fmt.Println(req.url)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestViewClient(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
id, _ := svc.AddClient(token, client)
client.ID = id
client.Key = id
data := toJSON(client)
cases := []struct {
desc string
id string
auth string
status int
res string
}{
{"view existing client", id, token, http.StatusOK, data},
{"view non-existent client", wrongID, token, http.StatusNotFound, ""},
{"view client by passing invalid id", "1", token, http.StatusNotFound, ""},
{"view client by passing invalid token", id, invalidToken, http.StatusForbidden, ""},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodGet,
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
body, err := ioutil.ReadAll(res.Body)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
data := strings.Trim(string(body), "\n")
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.Equal(t, tc.res, data, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data))
}
}
func TestListClients(t *testing.T) {
noClientsToken := "no_clients_token"
svc := newService(map[string]string{
token: email,
noClientsToken: "no_clients_user@example.com",
})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
data := []clients.Client{}
for i := 0; i < 101; i++ {
id, _ := svc.AddClient(token, client)
client.ID = id
client.Key = id
data = append(data, client)
}
clientURL := fmt.Sprintf("%s/clients", ts.URL)
cases := []struct {
desc string
auth string
status int
url string
res []clients.Client
}{
{"get a list of clients", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 5), data[0:5]},
{"get a list of clients with invalid token", invalidToken, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 1), nil},
{"get a list of clients with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, -1, 5), nil},
{"get a list of clients with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, -5), nil},
{"get a list of clients with zero limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 1, 0), nil},
{"get a list of clients with no offset provided", token, http.StatusOK, fmt.Sprintf("%s?limit=%d", clientURL, 5), data[0:5]},
{"get a list of clients with no limit provided", token, http.StatusOK, fmt.Sprintf("%s?offset=%d", clientURL, 1), data[1:11]},
{"get a list of clients with redundant query params", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", clientURL, 0, 5), data[0:5]},
{"get a list of clients with limit greater than max", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", clientURL, 0, 110), nil},
{"get a list of clients with default URL", token, http.StatusOK, fmt.Sprintf("%s%s", clientURL, ""), data[0:10]},
{"get a list of clients with invalid URL", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?%%"), nil},
{"get a list of clients with invalid number of params", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
{"get a list of clients with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=e&limit=5"), nil},
{"get a list of clients with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s%s", clientURL, "?offset=5&limit=e"), nil},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodGet,
url: tc.url,
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var data map[string][]clients.Client
json.NewDecoder(res.Body).Decode(&data)
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.ElementsMatch(t, tc.res, data["clients"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, data["clients"]))
}
}
func TestRemoveClient(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
id, _ := svc.AddClient(token, client)
cases := []struct {
desc string
id string
auth string
status int
}{
{"delete existing client", id, token, http.StatusNoContent},
{"delete non-existent client", wrongID, token, http.StatusNoContent},
{"delete client with invalid id", "1", token, http.StatusNoContent},
{"delete client with invalid token", id, invalidToken, http.StatusForbidden},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodDelete,
url: fmt.Sprintf("%s/clients/%s", ts.URL, tc.id),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestCreateChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
data := toJSON(channel)
cases := []struct {
desc string
req string
contentType string
auth string
status int
location string
}{
{"create new channel", data, contentType, token, http.StatusCreated, fmt.Sprintf("/channels/%s", id)},
{"create new channel with invalid token", data, contentType, invalidToken, http.StatusForbidden, ""},
{"create new channel with invalid data format", "{", contentType, token, http.StatusBadRequest, ""},
{"create new channel with empty JSON request", "{}", contentType, token, http.StatusCreated, "/channels/123e4567-e89b-12d3-a456-000000000002"},
{"create new channel with empty request", "", contentType, token, http.StatusBadRequest, ""},
{"create new channel with missing content type", data, "", token, http.StatusUnsupportedMediaType, ""},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodPost,
url: fmt.Sprintf("%s/channels", ts.URL),
contentType: tc.contentType,
token: tc.auth,
body: strings.NewReader(tc.req),
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
location := res.Header.Get("Location")
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.Equal(t, tc.location, location, fmt.Sprintf("%s: expected location %s got %s", tc.desc, tc.location, location))
}
}
func TestUpdateChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
updateData := toJSON(map[string]string{
"name": "updated_channel",
})
id, _ := svc.CreateChannel(token, channel)
cases := []struct {
desc string
req string
id string
contentType string
auth string
status int
}{
{"update existing channel", updateData, id, contentType, token, http.StatusOK},
{"update non-existing channel", updateData, wrongID, contentType, token, http.StatusNotFound},
{"update channel with invalid token", updateData, id, contentType, invalidToken, http.StatusForbidden},
{"update channel with invalid id", updateData, "1", contentType, token, http.StatusNotFound},
{"update channel with invalid data format", "}", id, contentType, token, http.StatusBadRequest},
{"update channel with empty JSON object", "{}", id, contentType, token, http.StatusOK},
{"update channel with empty request", "", id, contentType, token, http.StatusBadRequest},
{"update channel with missing content type", updateData, id, "", token, http.StatusUnsupportedMediaType},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodPut,
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
contentType: tc.contentType,
token: tc.auth,
body: strings.NewReader(tc.req),
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestViewChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
id, _ := svc.CreateChannel(token, channel)
channel.ID = id
data := toJSON(channel)
cases := []struct {
desc string
id string
auth string
status int
res string
}{
{"view existing channel", id, token, http.StatusOK, data},
{"view non-existent channel", wrongID, token, http.StatusNotFound, ""},
{"view channel with invalid id", "1", token, http.StatusNotFound, ""},
{"view channel with invalid token", id, invalidToken, http.StatusForbidden, ""},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodGet,
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
data, err := ioutil.ReadAll(res.Body)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
body := strings.Trim(string(data), "\n")
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.Equal(t, tc.res, body, fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body))
}
}
func TestListChannels(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
channels := []clients.Channel{}
for i := 0; i < 101; i++ {
id, _ := svc.CreateChannel(token, channel)
channel.ID = id
channels = append(channels, channel)
}
channelURL := fmt.Sprintf("%s/channels", ts.URL)
cases := []struct {
desc string
auth string
status int
url string
res []clients.Channel
}{
{"get a list of channels", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 6), channels[0:6]},
{"get a list of channels with invalid token", invalidToken, http.StatusForbidden, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 1), nil},
{"get a list of channels with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
{"get a list of channels with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, -1, 5), nil},
{"get a list of channels with zero limit", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 1, 0), nil},
{"get a list of channels with no offset provided", token, http.StatusOK, fmt.Sprintf("%s?limit=%d", channelURL, 5), channels[0:5]},
{"get a list of channels with no limit provided", token, http.StatusOK, fmt.Sprintf("%s?offset=%d", channelURL, 1), channels[1:11]},
{"get a list of channels with redundant query params", token, http.StatusOK, fmt.Sprintf("%s?offset=%d&limit=%d&value=something", channelURL, 0, 5), channels[0:5]},
{"get a list of channels with limit greater than max", token, http.StatusBadRequest, fmt.Sprintf("%s?offset=%d&limit=%d", channelURL, 0, 110), nil},
{"get a list of channels with default URL", token, http.StatusOK, fmt.Sprintf("%s%s", channelURL, ""), channels[0:10]},
{"get a list of channels with invalid URL", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?%%"), nil},
{"get a list of channels with invalid number of params", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=4&limit=4&limit=5&offset=5"), nil},
{"get a list of channels with invalid offset", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=e&limit=5"), nil},
{"get a list of channels with invalid limit", token, http.StatusBadRequest, fmt.Sprintf("%s%s", channelURL, "?offset=5&limit=e"), nil},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodGet,
url: tc.url,
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var body map[string][]clients.Channel
json.NewDecoder(res.Body).Decode(&body)
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
assert.ElementsMatch(t, tc.res, body["channels"], fmt.Sprintf("%s: expected body %s got %s", tc.desc, tc.res, body["channels"]))
}
}
func TestRemoveChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
ts := newServer(svc)
defer ts.Close()
client := ts.Client()
id, _ := svc.CreateChannel(token, channel)
channel.ID = id
cases := []struct {
desc string
id string
auth string
status int
}{
{"remove existing channel", channel.ID, token, http.StatusNoContent},
{"remove non-existent channel", channel.ID, token, http.StatusNoContent},
{"remove channel with invalid id", wrongID, token, http.StatusNoContent},
{"remove channel with invalid token", channel.ID, "invalidToken", http.StatusForbidden},
}
for _, tc := range cases {
req := testRequest{
client: client,
method: http.MethodDelete,
url: fmt.Sprintf("%s/channels/%s", ts.URL, tc.id),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestConnect(t *testing.T) {
otherToken := "other_token"
otherEmail := "other_user@example.com"
svc := newService(map[string]string{
token: email,
otherToken: otherEmail,
})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
clientID, _ := svc.AddClient(token, client)
chanID, _ := svc.CreateChannel(token, channel)
otherClientID, _ := svc.AddClient(otherToken, client)
otherChanID, _ := svc.CreateChannel(otherToken, channel)
cases := []struct {
desc string
chanID string
clientID string
auth string
status int
}{
{"connect existing client to existing channel", chanID, clientID, token, http.StatusOK},
{"connect existing client to non-existent channel", wrongID, clientID, token, http.StatusNotFound},
{"connect client with invalid id to channel", chanID, "1", token, http.StatusNotFound},
{"connect client to channel with invalid id", "1", clientID, token, http.StatusNotFound},
{"connect existing client to existing channel with invalid token", chanID, clientID, invalidToken, http.StatusForbidden},
{"connect client from owner to channel of other user", otherChanID, clientID, token, http.StatusNotFound},
{"connect client from other user to owner's channel", chanID, otherClientID, token, http.StatusNotFound},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodPut,
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
func TestDisconnnect(t *testing.T) {
otherToken := "other_token"
otherEmail := "other_user@example.com"
svc := newService(map[string]string{
token: email,
otherToken: otherEmail,
})
ts := newServer(svc)
defer ts.Close()
cli := ts.Client()
clientID, _ := svc.AddClient(token, client)
chanID, _ := svc.CreateChannel(token, channel)
svc.Connect(token, chanID, clientID)
otherClientID, _ := svc.AddClient(otherToken, client)
otherChanID, _ := svc.CreateChannel(otherToken, channel)
svc.Connect(otherToken, otherChanID, otherClientID)
cases := []struct {
desc string
chanID string
clientID string
auth string
status int
}{
{"disconnect connected client from channel", chanID, clientID, token, http.StatusNoContent},
{"disconnect non-connected client from channel", chanID, clientID, token, http.StatusNotFound},
{"disconnect non-existent client from channel", chanID, "1", token, http.StatusNotFound},
{"disconnect client from non-existent channel", "1", clientID, token, http.StatusNotFound},
{"disconnect client from channel with invalid token", chanID, clientID, invalidToken, http.StatusForbidden},
{"disconnect owner's client from someone elses channel", otherChanID, clientID, token, http.StatusNotFound},
{"disconnect other's client from owner's channel", chanID, otherClientID, token, http.StatusNotFound},
}
for _, tc := range cases {
req := testRequest{
client: cli,
method: http.MethodDelete,
url: fmt.Sprintf("%s/channels/%s/clients/%s", ts.URL, tc.chanID, tc.clientID),
token: tc.auth,
}
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
}
}
-139
View File
@@ -1,139 +0,0 @@
package http
import (
"github.com/asaskevich/govalidator"
"github.com/mainflux/mainflux/clients"
)
const maxLimitSize = 100
type apiReq interface {
validate() error
}
type identityReq struct {
key string
}
func (req identityReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
return nil
}
type addClientReq struct {
key string
client clients.Client
}
func (req addClientReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
return req.client.Validate()
}
type updateClientReq struct {
key string
id string
client clients.Client
}
func (req updateClientReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return clients.ErrNotFound
}
return req.client.Validate()
}
type createChannelReq struct {
key string
channel clients.Channel
}
func (req createChannelReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
return nil
}
type updateChannelReq struct {
key string
id string
channel clients.Channel
}
func (req updateChannelReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return clients.ErrNotFound
}
return nil
}
type viewResourceReq struct {
key string
id string
}
func (req viewResourceReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.id) {
return clients.ErrNotFound
}
return nil
}
type listResourcesReq struct {
key string
offset int
limit int
}
func (req *listResourcesReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
if req.offset >= 0 && req.limit > 0 && req.limit <= maxLimitSize {
return nil
}
return clients.ErrMalformedEntity
}
type connectionReq struct {
key string
chanID string
clientID string
}
func (req connectionReq) validate() error {
if req.key == "" {
return clients.ErrUnauthorizedAccess
}
if !govalidator.IsUUID(req.chanID) || !govalidator.IsUUID(req.clientID) {
return clients.ErrNotFound
}
return nil
}
-186
View File
@@ -1,186 +0,0 @@
package http
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/clients"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
)
const wrong string = "?"
var (
client = clients.Client{Type: "app"}
channel = clients.Channel{}
)
func TestIdentityReqValidation(t *testing.T) {
cases := map[string]struct {
key string
err error
}{
"non-empty token": {uuid.NewV4().String(), nil},
"empty token": {"", clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
req := identityReq{tc.key}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestAddClientReqValidation(t *testing.T) {
key := uuid.NewV4().String()
cases := map[string]struct {
client clients.Client
key string
err error
}{
"valid client addition request": {client, key, nil},
"missing token": {client, "", clients.ErrUnauthorizedAccess},
"wrong client type": {clients.Client{Type: wrong}, key, clients.ErrMalformedEntity},
}
for desc, tc := range cases {
req := addClientReq{
key: tc.key,
client: tc.client,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateClientReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
client clients.Client
id string
key string
err error
}{
"valid client update request": {client, id, key, nil},
"non-uuid client ID": {client, wrong, key, clients.ErrNotFound},
"missing token": {client, id, "", clients.ErrUnauthorizedAccess},
"wrong client type": {clients.Client{Type: "invalid"}, id, key, clients.ErrMalformedEntity},
}
for desc, tc := range cases {
req := updateClientReq{
key: tc.key,
id: tc.id,
client: tc.client,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCreateChannelReqValidation(t *testing.T) {
key := uuid.NewV4().String()
cases := map[string]struct {
channel clients.Channel
key string
err error
}{
"valid channel creation request": {channel, key, nil},
"missing token": {channel, "", clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
req := createChannelReq{
key: tc.key,
channel: tc.channel,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateChannelReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
channel clients.Channel
id string
key string
err error
}{
"valid channel update request": {channel, id, key, nil},
"non-uuid channel ID": {channel, wrong, key, clients.ErrNotFound},
"missing token": {channel, id, "", clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
req := updateChannelReq{
key: tc.key,
id: tc.id,
channel: tc.channel,
}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewResourceReqValidation(t *testing.T) {
key := uuid.NewV4().String()
id := uuid.NewV4().String()
cases := map[string]struct {
id string
key string
err error
}{
"valid resource viewing request": {id, key, nil},
"missing token": {id, "", clients.ErrUnauthorizedAccess},
"non-uuid resource ID": {wrong, key, clients.ErrNotFound},
}
for desc, tc := range cases {
req := viewResourceReq{tc.key, tc.id}
err := req.validate()
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListResourcesReqValidation(t *testing.T) {
key := uuid.NewV4().String()
value := 10
cases := map[string]struct {
key string
offset int
limit int
err error
}{
"valid listing request": {key, value, value, nil},
"missing token": {"", value, value, clients.ErrUnauthorizedAccess},
"negative offset": {key, -value, value, clients.ErrMalformedEntity},
"zero limit": {key, value, 0, clients.ErrMalformedEntity},
"negative limit": {key, value, -value, clients.ErrMalformedEntity},
"too big limit": {key, value, 20 * value, clients.ErrMalformedEntity},
}
for desc, tc := range cases {
req := listResourcesReq{
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", desc, tc.err, err))
}
}
-145
View File
@@ -1,145 +0,0 @@
// +build !test
package api
import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux/clients"
)
var _ clients.Service = (*metricsMiddleware)(nil)
type metricsMiddleware struct {
counter metrics.Counter
latency metrics.Histogram
svc clients.Service
}
// MetricsMiddleware instruments core service by tracking request count and
// latency.
func MetricsMiddleware(svc clients.Service, counter metrics.Counter, latency metrics.Histogram) clients.Service {
return &metricsMiddleware{
counter: counter,
latency: latency,
svc: svc,
}
}
func (ms *metricsMiddleware) AddClient(key string, client clients.Client) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "add_client").Add(1)
ms.latency.With("method", "add_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.AddClient(key, client)
}
func (ms *metricsMiddleware) UpdateClient(key string, client clients.Client) error {
defer func(begin time.Time) {
ms.counter.With("method", "update_client").Add(1)
ms.latency.With("method", "update_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.UpdateClient(key, client)
}
func (ms *metricsMiddleware) ViewClient(key string, id string) (clients.Client, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_client").Add(1)
ms.latency.With("method", "view_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewClient(key, id)
}
func (ms *metricsMiddleware) ListClients(key string, offset, limit int) ([]clients.Client, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_clients").Add(1)
ms.latency.With("method", "list_clients").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListClients(key, offset, limit)
}
func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "remove_client").Add(1)
ms.latency.With("method", "remove_client").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.RemoveClient(key, id)
}
func (ms *metricsMiddleware) CreateChannel(key string, channel clients.Channel) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "create_channel").Add(1)
ms.latency.With("method", "create_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.CreateChannel(key, channel)
}
func (ms *metricsMiddleware) UpdateChannel(key string, channel clients.Channel) error {
defer func(begin time.Time) {
ms.counter.With("method", "update_channel").Add(1)
ms.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.UpdateChannel(key, channel)
}
func (ms *metricsMiddleware) ViewChannel(key string, id string) (clients.Channel, error) {
defer func(begin time.Time) {
ms.counter.With("method", "view_channel").Add(1)
ms.latency.With("method", "view_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ViewChannel(key, id)
}
func (ms *metricsMiddleware) ListChannels(key string, offset, limit int) ([]clients.Channel, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_channels").Add(1)
ms.latency.With("method", "list_channels").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.ListChannels(key, offset, limit)
}
func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
defer func(begin time.Time) {
ms.counter.With("method", "remove_channel").Add(1)
ms.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.RemoveChannel(key, id)
}
func (ms *metricsMiddleware) Connect(key, chanID, clientID string) error {
defer func(begin time.Time) {
ms.counter.With("method", "connect").Add(1)
ms.latency.With("method", "connect").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Connect(key, chanID, clientID)
}
func (ms *metricsMiddleware) Disconnect(key, chanID, clientID string) error {
defer func(begin time.Time) {
ms.counter.With("method", "disconnect").Add(1)
ms.latency.With("method", "disconnect").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Disconnect(key, chanID, clientID)
}
func (ms *metricsMiddleware) CanAccess(key string, id string) (string, error) {
defer func(begin time.Time) {
ms.counter.With("method", "can_access").Add(1)
ms.latency.With("method", "can_access").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.CanAccess(key, id)
}
-31
View File
@@ -1,31 +0,0 @@
// Package bcrypt provides a hasher implementation utilising bcrypt.
package bcrypt
import (
"github.com/mainflux/mainflux/clients"
"golang.org/x/crypto/bcrypt"
)
const cost int = 10
var _ clients.Hasher = (*bcryptHasher)(nil)
type bcryptHasher struct{}
// New instantiates a bcrypt-based hasher implementation.
func New() clients.Hasher {
return &bcryptHasher{}
}
func (bh *bcryptHasher) Hash(pwd string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), cost)
if err != nil {
return "", err
}
return string(hash), nil
}
func (bh *bcryptHasher) Compare(plain, hashed string) error {
return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain))
}
-44
View File
@@ -1,44 +0,0 @@
package clients
// Channel represents a Mainflux "communication group". This group contains the
// clients that can exchange messages between eachother.
type Channel struct {
ID string `json:"id"`
Owner string `json:"-"`
Name string `json:"name,omitempty"`
Clients []Client `json:"connected,omitempty"`
}
// ChannelRepository specifies a channel persistence API.
type ChannelRepository interface {
// Save persists the channel. Successful operation is indicated by unique
// identifier accompanied by nil error response. A non-nil error is
// returned to indicate operation failure.
Save(Channel) (string, error)
// Update performs an update to the existing channel. A non-nil error is
// returned to indicate operation failure.
Update(Channel) error
// One retrieves the channel having the provided identifier, that is owned
// by the specified user.
One(string, string) (Channel, error)
// All retrieves the subset of channels owned by the specified user.
All(string, int, int) []Channel
// Remove removes the channel having the provided identifier, that is owned
// by the specified user.
Remove(string, string) error
// Connect adds client to the channel's list of connected clients.
Connect(string, string, string) error
// Disconnect removes client from the channel's list of connected
// clients.
Disconnect(string, string, string) error
// HasClient determines whether the client with the provided identifier, is
// "connected" to the specified channel.
HasClient(string, string) bool
}
-53
View File
@@ -1,53 +0,0 @@
package clients
import "strings"
// Client represents a Mainflux client. Each client is owned by one user, and
// it is assigned with the unique identifier and (temporary) access key.
type Client struct {
ID string `json:"id"`
Owner string `json:"-"`
Type string `json:"type"`
Name string `json:"name,omitempty"`
Key string `json:"key"`
Payload string `json:"payload,omitempty"`
}
var clientTypes = map[string]bool{
"app": true,
"device": true,
}
// Validate returns an error if client representation is invalid.
func (c *Client) Validate() error {
if c.Type = strings.ToLower(c.Type); !clientTypes[c.Type] {
return ErrMalformedEntity
}
return nil
}
// ClientRepository specifies a client persistence API.
type ClientRepository interface {
// ID generates new resource identifier.
ID() string
// Save persists the client. Successful operation is indicated by non-nil
// error response.
Save(Client) error
// Update performs an update to the existing client. A non-nil error is
// returned to indicate operation failure.
Update(Client) error
// One retrieves the client having the provided identifier, that is owned
// by the specified user.
One(string, string) (Client, error)
// All retrieves the subset of clients owned by the specified user.
All(string, int, int) []Client
// Remove removes the client having the provided identifier, that is owned
// by the specified user.
Remove(string, string) error
}
-3
View File
@@ -1,3 +0,0 @@
// Package clients contains the domain concept definitions needed to support
// Mainflux clients service functionality.
package clients
-74
View File
@@ -1,74 +0,0 @@
// Package jwt provides a JWT identity provider.
package jwt
import (
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/mainflux/mainflux/clients"
)
const (
issuer string = "mainflux"
duration time.Duration = 10 * time.Hour
)
var _ clients.IdentityProvider = (*jwtIdentityProvider)(nil)
type jwtIdentityProvider struct {
secret string
}
// New instantiates a JWT identity provider.
func New(secret string) clients.IdentityProvider {
return &jwtIdentityProvider{secret}
}
func (idp *jwtIdentityProvider) TemporaryKey(id string) (string, error) {
now := time.Now().UTC()
exp := now.Add(duration)
claims := jwt.StandardClaims{
Subject: id,
Issuer: issuer,
IssuedAt: now.Unix(),
ExpiresAt: exp.Unix(),
}
return idp.jwt(claims)
}
func (idp *jwtIdentityProvider) PermanentKey(id string) (string, error) {
claims := jwt.StandardClaims{
Subject: id,
Issuer: issuer,
IssuedAt: time.Now().UTC().Unix(),
}
return idp.jwt(claims)
}
func (idp *jwtIdentityProvider) jwt(claims jwt.StandardClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(idp.secret))
}
func (idp *jwtIdentityProvider) Identity(key string) (string, error) {
token, err := jwt.Parse(key, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, clients.ErrUnauthorizedAccess
}
return []byte(idp.secret), nil
})
if err != nil {
return "", clients.ErrUnauthorizedAccess
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims["sub"].(string), nil
}
return "", clients.ErrUnauthorizedAccess
}
-144
View File
@@ -1,144 +0,0 @@
package mocks
import (
"fmt"
"strings"
"sync"
"github.com/mainflux/mainflux/clients"
)
var _ clients.ChannelRepository = (*channelRepositoryMock)(nil)
const chanID = "123e4567-e89b-12d3-a456-"
type channelRepositoryMock struct {
mu sync.Mutex
counter int
channels map[string]clients.Channel
clients clients.ClientRepository
}
// NewChannelRepository creates in-memory channel repository.
func NewChannelRepository(repo clients.ClientRepository) clients.ChannelRepository {
return &channelRepositoryMock{
channels: make(map[string]clients.Channel),
clients: repo,
}
}
func (crm *channelRepositoryMock) Save(channel clients.Channel) (string, error) {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.counter++
channel.ID = fmt.Sprintf("%s%012d", chanID, crm.counter)
crm.channels[key(channel.Owner, channel.ID)] = channel
return channel.ID, nil
}
func (crm *channelRepositoryMock) Update(channel clients.Channel) error {
crm.mu.Lock()
defer crm.mu.Unlock()
dbKey := key(channel.Owner, channel.ID)
if _, ok := crm.channels[dbKey]; !ok {
return clients.ErrNotFound
}
crm.channels[dbKey] = channel
return nil
}
func (crm *channelRepositoryMock) One(owner, id string) (clients.Channel, error) {
if c, ok := crm.channels[key(owner, id)]; ok {
return c, nil
}
return clients.Channel{}, clients.ErrNotFound
}
func (crm *channelRepositoryMock) All(owner string, offset, limit int) []clients.Channel {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner)
channels := make([]clients.Channel, 0)
if offset < 0 || limit <= 0 {
return channels
}
// Since IDs starts from 1, shift everything by one.
first := fmt.Sprintf("%s%012d", chanID, offset+1)
last := fmt.Sprintf("%s%012d", chanID, offset+limit+1)
for k, v := range crm.channels {
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
channels = append(channels, v)
}
}
return channels
}
func (crm *channelRepositoryMock) Remove(owner, id string) error {
delete(crm.channels, key(owner, id))
return nil
}
func (crm *channelRepositoryMock) Connect(owner, chanID, clientID string) error {
channel, err := crm.One(owner, chanID)
if err != nil {
return err
}
client, err := crm.clients.One(owner, clientID)
if err != nil {
return err
}
channel.Clients = append(channel.Clients, client)
return crm.Update(channel)
}
func (crm *channelRepositoryMock) Disconnect(owner, chanID, clientID string) error {
channel, err := crm.One(owner, chanID)
if err != nil {
return err
}
if !crm.HasClient(chanID, clientID) {
return clients.ErrNotFound
}
connected := make([]clients.Client, len(channel.Clients)-1)
for _, client := range channel.Clients {
if client.ID != clientID {
connected = append(connected, client)
}
}
channel.Clients = connected
return crm.Update(channel)
}
func (crm *channelRepositoryMock) HasClient(channel, client string) bool {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
suffix := fmt.Sprintf("-%s", channel)
for k, v := range crm.channels {
if strings.HasSuffix(k, suffix) {
for _, c := range v.Clients {
if c.ID == client {
return true
}
}
break
}
}
return false
}
-94
View File
@@ -1,94 +0,0 @@
package mocks
import (
"fmt"
"strings"
"sync"
"github.com/mainflux/mainflux/clients"
)
var _ clients.ClientRepository = (*clientRepositoryMock)(nil)
const cliID = "123e4567-e89b-12d3-a456-"
type clientRepositoryMock struct {
mu sync.Mutex
counter int
clients map[string]clients.Client
}
// NewClientRepository creates in-memory client repository.
func NewClientRepository() clients.ClientRepository {
return &clientRepositoryMock{
clients: make(map[string]clients.Client),
}
}
func (crm *clientRepositoryMock) ID() string {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.counter++
return fmt.Sprintf("%s%012d", cliID, crm.counter)
}
func (crm *clientRepositoryMock) Save(client clients.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
crm.clients[key(client.Owner, client.ID)] = client
return nil
}
func (crm *clientRepositoryMock) Update(client clients.Client) error {
crm.mu.Lock()
defer crm.mu.Unlock()
dbKey := key(client.Owner, client.ID)
if _, ok := crm.clients[dbKey]; !ok {
return clients.ErrNotFound
}
crm.clients[dbKey] = client
return nil
}
func (crm *clientRepositoryMock) One(owner, id string) (clients.Client, error) {
if c, ok := crm.clients[key(owner, id)]; ok {
return c, nil
}
return clients.Client{}, clients.ErrNotFound
}
func (crm *clientRepositoryMock) All(owner string, offset, limit int) []clients.Client {
// This obscure way to examine map keys is enforced by the key structure
// itself (see mocks/commons.go).
prefix := fmt.Sprintf("%s-", owner)
clients := make([]clients.Client, 0)
if offset < 0 || limit <= 0 {
return clients
}
// Since IDs start from 1, shift everything by one.
first := fmt.Sprintf("%s%012d", cliID, offset+1)
last := fmt.Sprintf("%s%012d", cliID, offset+limit+1)
for k, v := range crm.clients {
if strings.HasPrefix(k, prefix) && v.ID >= first && v.ID < last {
clients = append(clients, v)
}
}
return clients
}
func (crm *clientRepositoryMock) Remove(owner, id string) error {
delete(crm.clients, key(owner, id))
return nil
}
-54
View File
@@ -1,54 +0,0 @@
package mocks
import (
"github.com/mainflux/mainflux/clients"
)
var (
_ clients.Hasher = (*hasherMock)(nil)
_ clients.IdentityProvider = (*identityProviderMock)(nil)
)
type hasherMock struct{}
func (hm *hasherMock) Hash(pwd string) (string, error) {
return pwd, nil
}
func (hm *hasherMock) Compare(plain, hashed string) error {
if plain != hashed {
return clients.ErrUnauthorizedAccess
}
return nil
}
type identityProviderMock struct{}
func (idp *identityProviderMock) TemporaryKey(id string) (string, error) {
if id == "" {
return "", clients.ErrUnauthorizedAccess
}
return id, nil
}
func (idp *identityProviderMock) PermanentKey(id string) (string, error) {
return idp.TemporaryKey(id)
}
func (idp *identityProviderMock) Identity(key string) (string, error) {
return idp.TemporaryKey(key)
}
// NewHasher creates "no-op" hasher for test purposes. This implementation will
// return secrets without changing them.
func NewHasher() clients.Hasher {
return &hasherMock{}
}
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
// token will hold value provided by the caller.
func NewIdentityProvider() clients.IdentityProvider {
return &identityProviderMock{}
}
-179
View File
@@ -1,179 +0,0 @@
package postgres
import (
"database/sql"
"fmt"
"github.com/lib/pq"
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/logger"
uuid "github.com/satori/go.uuid"
)
var _ clients.ChannelRepository = (*channelRepository)(nil)
const (
errDuplicate = "unique_violation"
errFK = "foreign_key_violation"
)
type channelRepository struct {
db *sql.DB
log logger.Logger
}
// NewChannelRepository instantiates a PostgreSQL implementation of channel
// repository.
func NewChannelRepository(db *sql.DB, log logger.Logger) clients.ChannelRepository {
return &channelRepository{db: db, log: log}
}
func (cr channelRepository) Save(channel clients.Channel) (string, error) {
channel.ID = uuid.NewV4().String()
q := `INSERT INTO channels (id, owner, name) VALUES ($1, $2, $3)`
_, err := cr.db.Exec(q, channel.ID, channel.Owner, channel.Name)
if err != nil {
return "", err
}
return channel.ID, nil
}
func (cr channelRepository) Update(channel clients.Channel) error {
q := `UPDATE channels SET name = $1 WHERE owner = $2 AND id = $3;`
res, err := cr.db.Exec(q, channel.Name, channel.Owner, channel.ID)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
if cnt == 0 {
return clients.ErrNotFound
}
return nil
}
func (cr channelRepository) One(owner, id string) (clients.Channel, error) {
q := `SELECT name FROM channels WHERE id = $1 AND owner = $2`
channel := clients.Channel{ID: id, Owner: owner}
if err := cr.db.QueryRow(q, id, owner).Scan(&channel.Name); err != nil {
empty := clients.Channel{}
if err == sql.ErrNoRows {
return empty, clients.ErrNotFound
}
return empty, err
}
qr := `SELECT id, type, name, key, payload FROM clients cli
INNER JOIN connections conn
ON cli.id = conn.client_id AND cli.owner = conn.client_owner
WHERE conn.channel_id = $1 AND conn.channel_owner = $2`
rows, err := cr.db.Query(qr, id, owner)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return clients.Channel{}, err
}
defer rows.Close()
for rows.Next() {
c := clients.Client{Owner: owner}
if err = rows.Scan(&c.ID, &c.Name, &c.Type, &c.Key, &c.Payload); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
return clients.Channel{}, err
}
channel.Clients = append(channel.Clients, c)
}
return channel, nil
}
func (cr channelRepository) All(owner string, offset, limit int) []clients.Channel {
q := `SELECT id, name FROM channels WHERE owner = $1 LIMIT $2 OFFSET $3`
items := []clients.Channel{}
rows, err := cr.db.Query(q, owner, limit, offset)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve channels due to %s", err))
return []clients.Channel{}
}
defer rows.Close()
for rows.Next() {
c := clients.Channel{Owner: owner}
if err = rows.Scan(&c.ID, &c.Name); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved channel due to %s", err))
return []clients.Channel{}
}
items = append(items, c)
}
return items
}
func (cr channelRepository) Remove(owner, id string) error {
q := `DELETE FROM channels WHERE id = $1 AND owner = $2`
cr.db.Exec(q, id, owner)
return nil
}
func (cr channelRepository) Connect(owner, chanID, clientID string) error {
q := `INSERT INTO connections (channel_id, channel_owner, client_id, client_owner) VALUES ($1, $2, $3, $2)`
if _, err := cr.db.Exec(q, chanID, owner, clientID); err != nil {
pqErr, ok := err.(*pq.Error)
if ok && errFK == pqErr.Code.Name() {
return clients.ErrNotFound
}
// connect is idempotent
if ok && errDuplicate == pqErr.Code.Name() {
return nil
}
return err
}
return nil
}
func (cr channelRepository) Disconnect(owner, chanID, clientID string) error {
q := `DELETE FROM connections
WHERE channel_id = $1 AND channel_owner = $2
AND client_id = $3 AND client_owner = $2`
res, err := cr.db.Exec(q, chanID, owner, clientID)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
if cnt == 0 {
return clients.ErrNotFound
}
return nil
}
func (cr channelRepository) HasClient(chanID, clientID string) bool {
q := "SELECT EXISTS (SELECT 1 FROM connections WHERE channel_id = $1 AND client_id = $2);"
exists := false
if err := cr.db.QueryRow(q, chanID, clientID).Scan(&exists); err != nil {
cr.log.Error(fmt.Sprintf("Failed to check client existence due to %s", err))
}
return exists
}
-215
View File
@@ -1,215 +0,0 @@
package postgres_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/clients/postgres"
"github.com/stretchr/testify/assert"
)
func TestChannelSave(t *testing.T) {
email := "channel-save@example.com"
channel := clients.Channel{Owner: email}
channelRepo := postgres.NewChannelRepository(db, testLog)
_, err := channelRepo.Save(channel)
hasErr := err != nil
assert.False(t, hasErr, fmt.Sprintf("create new channel: expected false got %t", hasErr))
}
func TestChannelUpdate(t *testing.T) {
email := "channel-update@example.com"
chanRepo := postgres.NewChannelRepository(db, testLog)
c := clients.Channel{Owner: email}
id, _ := chanRepo.Save(c)
c.ID = id
cases := map[string]struct {
channel clients.Channel
err error
}{
"existing channel": {c, nil},
"non-existing channel with existing user": {clients.Channel{ID: wrong, Owner: email}, clients.ErrNotFound},
"non-existing channel with non-existing user": {clients.Channel{ID: wrong, Owner: wrong}, clients.ErrNotFound},
}
for desc, tc := range cases {
err := chanRepo.Update(tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestSingleChannelRetrieval(t *testing.T) {
email := "channel-single-retrieval@example.com"
chanRepo := postgres.NewChannelRepository(db, testLog)
c := clients.Channel{Owner: email}
id, _ := chanRepo.Save(c)
cases := map[string]struct {
owner string
ID string
err error
}{
"existing user": {c.Owner, id, nil},
"existing user, non-existing channel": {c.Owner, wrong, clients.ErrNotFound},
"non-existing owner": {wrong, id, clients.ErrNotFound},
}
for desc, tc := range cases {
_, err := chanRepo.One(tc.owner, tc.ID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestMultiChannelRetrieval(t *testing.T) {
email := "channel-multi-retrieval@example.com"
chanRepo := postgres.NewChannelRepository(db, testLog)
n := 10
for i := 0; i < n; i++ {
c := clients.Channel{Owner: email}
chanRepo.Save(c)
}
cases := map[string]struct {
owner string
offset int
limit int
size int
}{
"existing owner": {email, 0, n, n},
"non-existing owner": {wrong, 1, 6, 0},
}
for desc, tc := range cases {
size := len(chanRepo.All(tc.owner, tc.offset, tc.limit))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
}
}
func TestChannelRemoval(t *testing.T) {
email := "channel-removal@example.com"
chanRepo := postgres.NewChannelRepository(db, testLog)
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
// show that the removal works the same for both existing and non-existing
// (removed) channel
for i := 0; i < 2; i++ {
if err := chanRepo.Remove(email, chanID); err != nil {
t.Fatalf("#%d: failed to remove channel due to: %s", i, err)
}
if _, err := chanRepo.One(email, chanID); err != clients.ErrNotFound {
t.Fatalf("#%d: expected %s got %s", i, clients.ErrNotFound, err)
}
}
}
func TestChannelConnect(t *testing.T) {
email := "channel-connect@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
client := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db, testLog)
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
cases := []struct {
desc string
owner string
chanID string
clientID string
err error
}{
{"existing user, channel and client", email, chanID, client.ID, nil},
{"connected channel and client", email, chanID, client.ID, nil},
{"with non-existing user", wrong, chanID, client.ID, clients.ErrNotFound},
{"non-existing channel", email, wrong, client.ID, clients.ErrNotFound},
{"non-existing client", email, chanID, wrong, clients.ErrNotFound},
}
for _, tc := range cases {
err := chanRepo.Connect(tc.owner, tc.chanID, tc.clientID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestChannelDisconnect(t *testing.T) {
email := "channel-disconnect@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
client := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db, testLog)
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
chanRepo.Connect(email, chanID, client.ID)
cases := []struct {
desc string
owner string
chanID string
clientID string
err error
}{
{"connected client", email, chanID, client.ID, nil},
{"non-connected client", email, chanID, client.ID, clients.ErrNotFound},
{"non-existing user", wrong, chanID, client.ID, clients.ErrNotFound},
{"non-existing channel", email, wrong, client.ID, clients.ErrNotFound},
{"non-existing client", email, chanID, wrong, clients.ErrNotFound},
}
for _, tc := range cases {
err := chanRepo.Disconnect(tc.owner, tc.chanID, tc.clientID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestChannelAccessCheck(t *testing.T) {
email := "channel-access-check@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
client := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(client)
chanRepo := postgres.NewChannelRepository(db, testLog)
chanID, _ := chanRepo.Save(clients.Channel{Owner: email})
chanRepo.Connect(email, chanID, client.ID)
cases := map[string]struct {
chanID string
clientID string
hasAccess bool
}{
"client that has access": {chanID, client.ID, true},
"client without access": {chanID, wrong, false},
"check access to non-existing channel": {wrong, client.ID, false},
}
for desc, tc := range cases {
hasAccess := chanRepo.HasClient(tc.chanID, tc.clientID)
assert.Equal(t, tc.hasAccess, hasAccess, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasAccess, hasAccess))
}
}
-101
View File
@@ -1,101 +0,0 @@
package postgres
import (
"database/sql"
"fmt"
_ "github.com/lib/pq" // required for DB access
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/logger"
uuid "github.com/satori/go.uuid"
)
var _ clients.ClientRepository = (*clientRepository)(nil)
type clientRepository struct {
db *sql.DB
log logger.Logger
}
// NewClientRepository instantiates a PostgreSQL implementation of client
// repository.
func NewClientRepository(db *sql.DB, log logger.Logger) clients.ClientRepository {
return &clientRepository{db: db, log: log}
}
func (cr clientRepository) ID() string {
return uuid.NewV4().String()
}
func (cr clientRepository) Save(client clients.Client) error {
q := `INSERT INTO clients (id, owner, type, name, key, payload) VALUES ($1, $2, $3, $4, $5, $6)`
_, err := cr.db.Exec(q, client.ID, client.Owner, client.Type, client.Name, client.Key, client.Payload)
return err
}
func (cr clientRepository) Update(client clients.Client) error {
q := `UPDATE clients SET name = $1, payload = $2 WHERE owner = $3 AND id = $4;`
res, err := cr.db.Exec(q, client.Name, client.Payload, client.Owner, client.ID)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
if cnt == 0 {
return clients.ErrNotFound
}
return nil
}
func (cr clientRepository) One(owner, id string) (clients.Client, error) {
q := `SELECT name, type, key, payload FROM clients WHERE id = $1 AND owner = $2`
client := clients.Client{ID: id, Owner: owner}
err := cr.db.
QueryRow(q, id, owner).
Scan(&client.Name, &client.Type, &client.Key, &client.Payload)
if err != nil {
empty := clients.Client{}
if err == sql.ErrNoRows {
return empty, clients.ErrNotFound
}
return empty, err
}
return client, nil
}
func (cr clientRepository) All(owner string, offset, limit int) []clients.Client {
q := `SELECT id, name, type, key, payload FROM clients WHERE owner = $1 LIMIT $2 OFFSET $3`
items := []clients.Client{}
rows, err := cr.db.Query(q, owner, limit, offset)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve clients due to %s", err))
return []clients.Client{}
}
defer rows.Close()
for rows.Next() {
c := clients.Client{Owner: owner}
if err = rows.Scan(&c.ID, &c.Name, &c.Type, &c.Key, &c.Payload); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved client due to %s", err))
return []clients.Client{}
}
items = append(items, c)
}
return items
}
func (cr clientRepository) Remove(owner, id string) error {
q := `DELETE FROM clients WHERE id = $1 AND owner = $2`
cr.db.Exec(q, id, owner)
return nil
}
-133
View File
@@ -1,133 +0,0 @@
package postgres_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/clients/postgres"
"github.com/stretchr/testify/assert"
)
func TestClientSave(t *testing.T) {
email := "client-save@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
client := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
hasErr := clientRepo.Save(client) != nil
assert.False(t, hasErr, fmt.Sprintf("create new client: expected false got %t\n", hasErr))
}
func TestClientUpdate(t *testing.T) {
email := "client-update@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
c := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(c)
cases := map[string]struct {
client clients.Client
err error
}{
"existing client": {c, nil},
"non-existing client with existing user": {clients.Client{ID: wrong, Owner: email}, clients.ErrNotFound},
"non-existing client with non-existing user": {clients.Client{ID: wrong, Owner: wrong}, clients.ErrNotFound},
}
for desc, tc := range cases {
err := clientRepo.Update(tc.client)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestSingleClientRetrieval(t *testing.T) {
email := "client-single-retrieval@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
c := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(c)
cases := map[string]struct {
owner string
ID string
err error
}{
"existing user": {c.Owner, c.ID, nil},
"existing user, non-existing client": {c.Owner, wrong, clients.ErrNotFound},
"non-existing owner": {wrong, c.ID, clients.ErrNotFound},
}
for desc, tc := range cases {
_, err := clientRepo.One(tc.owner, tc.ID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestMultiClientRetrieval(t *testing.T) {
email := "client-multi-retrieval@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
n := 10
for i := 0; i < n; i++ {
c := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(c)
}
cases := map[string]struct {
owner string
offset int
limit int
size int
}{
"existing owner, retrieve all": {email, 0, n, n},
"existing owner, retrieve subset": {email, 1, 6, 6},
"non-existing owner": {wrong, 1, 6, 0},
}
for desc, tc := range cases {
n := len(clientRepo.All(tc.owner, tc.offset, tc.limit))
assert.Equal(t, tc.size, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, n))
}
}
func TestClientRemoval(t *testing.T) {
email := "client-removal@example.com"
clientRepo := postgres.NewClientRepository(db, testLog)
client := clients.Client{
ID: clientRepo.ID(),
Owner: email,
}
clientRepo.Save(client)
// show that the removal works the same for both existing and non-existing
// (removed) client
for i := 0; i < 2; i++ {
if err := clientRepo.Remove(email, client.ID); err != nil {
t.Fatalf("#%d: failed to remove client due to: %s", i, err)
}
if _, err := clientRepo.One(email, client.ID); err != clients.ErrNotFound {
t.Fatalf("#%d: expected %s got %s", i, clients.ErrNotFound, err)
}
}
}
-25
View File
@@ -1,25 +0,0 @@
package clients
// Hasher specifies an API for generating hashes of an arbitrary textual
// content.
type Hasher interface {
// Hash generates the hashed string from plain-text.
Hash(string) (string, error)
// Compare compares plain-text version to the hashed one. An error should
// indicate failed comparison.
Compare(string, string) error
}
// IdentityProvider specifies an API for identity management via security
// tokens.
type IdentityProvider interface {
// TemporaryKey generates the temporary access token.
TemporaryKey(string) (string, error)
// PermanentKey generates the non-expiring access token.
PermanentKey(string) (string, error)
// Identity extracts the entity identifier given its secret key.
Identity(string) (string, error)
}
-265
View File
@@ -1,265 +0,0 @@
package clients
import (
"context"
"errors"
"time"
"github.com/mainflux/mainflux"
)
var (
// ErrConflict indicates usage of the existing email during account
// registration.
ErrConflict = errors.New("email already taken")
// ErrMalformedEntity indicates malformed entity specification (e.g.
// invalid username or password).
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")
// ErrNotFound indicates a non-existent entity request.
ErrNotFound = errors.New("non-existent entity")
)
// Service specifies an API that must be fullfiled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
type Service interface {
// AddClient adds new client to the user identified by the provided key.
AddClient(string, Client) (string, error)
// UpdateClient updates the client identified by the provided ID, that
// belongs to the user identified by the provided key.
UpdateClient(string, Client) error
// ViewClient retrieves data about the client identified with the provided
// ID, that belongs to the user identified by the provided key.
ViewClient(string, string) (Client, error)
// ListClients retrieves data about subset of clients that belongs to the
// user identified by the provided key.
ListClients(string, int, int) ([]Client, error)
// RemoveClient removes the client identified with the provided ID, that
// belongs to the user identified by the provided key.
RemoveClient(string, string) error
// CreateChannel adds new channel to the user identified by the provided key.
CreateChannel(string, Channel) (string, error)
// UpdateChannel updates the channel identified by the provided ID, that
// belongs to the user identified by the provided key.
UpdateChannel(string, Channel) error
// ViewChannel retrieves data about the channel identified by the provided
// ID, that belongs to the user identified by the provided key.
ViewChannel(string, string) (Channel, error)
// ListChannels retrieves data about subset of channels that belongs to the
// user identified by the provided key.
ListChannels(string, int, int) ([]Channel, error)
// RemoveChannel removes the client identified by the provided ID, that
// belongs to the user identified by the provided key.
RemoveChannel(string, string) error
// Connect adds client to the channel's list of connected clients.
Connect(string, string, string) error
// Disconnect removes client from the channel's list of connected
// clients.
Disconnect(string, string, string) error
// CanAccess determines whether the channel can be accessed using the
// provided key and returns client's id.
CanAccess(string, string) (string, error)
}
var _ Service = (*clientsService)(nil)
type clientsService struct {
users mainflux.UsersServiceClient
clients ClientRepository
channels ChannelRepository
hasher Hasher
idp IdentityProvider
}
// New instantiates the clients service implementation.
func New(users mainflux.UsersServiceClient, clients ClientRepository, channels ChannelRepository, hasher Hasher, idp IdentityProvider) Service {
return &clientsService{
users: users,
clients: clients,
channels: channels,
hasher: hasher,
idp: idp,
}
}
func (ms *clientsService) AddClient(key string, client Client) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return "", ErrUnauthorizedAccess
}
client.ID = ms.clients.ID()
client.Owner = res.GetValue()
client.Key, _ = ms.idp.PermanentKey(client.ID)
return client.ID, ms.clients.Save(client)
}
func (ms *clientsService) UpdateClient(key string, client Client) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
client.Owner = res.GetValue()
return ms.clients.Update(client)
}
func (ms *clientsService) ViewClient(key, id string) (Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return Client{}, ErrUnauthorizedAccess
}
return ms.clients.One(res.GetValue(), id)
}
func (ms *clientsService) ListClients(key string, offset, limit int) ([]Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return nil, ErrUnauthorizedAccess
}
return ms.clients.All(res.GetValue(), offset, limit), nil
}
func (ms *clientsService) RemoveClient(key, id string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
return ms.clients.Remove(res.GetValue(), id)
}
func (ms *clientsService) CreateChannel(key string, channel Channel) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return "", ErrUnauthorizedAccess
}
channel.Owner = res.GetValue()
return ms.channels.Save(channel)
}
func (ms *clientsService) UpdateChannel(key string, channel Channel) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
channel.Owner = res.GetValue()
return ms.channels.Update(channel)
}
func (ms *clientsService) ViewChannel(key, id string) (Channel, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return Channel{}, ErrUnauthorizedAccess
}
return ms.channels.One(res.GetValue(), id)
}
func (ms *clientsService) ListChannels(key string, offset, limit int) ([]Channel, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return nil, ErrUnauthorizedAccess
}
return ms.channels.All(res.GetValue(), offset, limit), nil
}
func (ms *clientsService) RemoveChannel(key, id string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Remove(res.GetValue(), id)
}
func (ms *clientsService) Connect(key, chanID, clientID string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Connect(res.GetValue(), chanID, clientID)
}
func (ms *clientsService) Disconnect(key, chanID, clientID string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := ms.users.Identify(ctx, &mainflux.Token{Value: key})
if err != nil {
return ErrUnauthorizedAccess
}
return ms.channels.Disconnect(res.GetValue(), chanID, clientID)
}
func (ms *clientsService) CanAccess(key, channel string) (string, error) {
client, err := ms.idp.Identity(key)
if err != nil {
return "", err
}
if !ms.channels.HasClient(channel, client) {
return "", ErrUnauthorizedAccess
}
return client, nil
}
-342
View File
@@ -1,342 +0,0 @@
package clients_test
import (
"fmt"
"testing"
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/clients/mocks"
"github.com/stretchr/testify/assert"
)
const (
wrong = "wrong-value"
email = "user@example.com"
token = "token"
)
var (
client = clients.Client{Type: "app", Name: "test"}
channel = clients.Channel{Name: "test", Clients: []clients.Client{}}
)
func newService(tokens map[string]string) clients.Service {
users := mocks.NewUsersService(tokens)
clientsRepo := mocks.NewClientRepository()
channelsRepo := mocks.NewChannelRepository(clientsRepo)
hasher := mocks.NewHasher()
idp := mocks.NewIdentityProvider()
return clients.New(users, clientsRepo, channelsRepo, hasher, idp)
}
func TestAddClient(t *testing.T) {
svc := newService(map[string]string{token: email})
cases := map[string]struct {
client clients.Client
key string
err error
}{
"add new app": {clients.Client{Type: "app", Name: "a"}, token, nil},
"add new device": {clients.Client{Type: "device", Name: "b"}, token, nil},
"add client with wrong credentials": {clients.Client{Type: "app", Name: "d"}, wrong, clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.AddClient(tc.key, tc.client)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateClient(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
cases := map[string]struct {
client clients.Client
key string
err error
}{
"update existing client": {client, token, nil},
"update client with wrong credentials": {client, wrong, clients.ErrUnauthorizedAccess},
"update non-existing client": {clients.Client{ID: "2", Type: "app", Name: "d"}, token, clients.ErrNotFound},
}
for desc, tc := range cases {
err := svc.UpdateClient(tc.key, tc.client)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewClient(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
cases := map[string]struct {
id string
key string
err error
}{
"view existing client": {client.ID, token, nil},
"view client with wrong credentials": {client.ID, wrong, clients.ErrUnauthorizedAccess},
"view non-existing client": {wrong, token, clients.ErrNotFound},
}
for desc, tc := range cases {
_, err := svc.ViewClient(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListClients(t *testing.T) {
svc := newService(map[string]string{token: email})
n := 10
for i := 0; i < n; i++ {
svc.AddClient(token, client)
}
cases := map[string]struct {
key string
offset int
limit int
size int
err error
}{
"list clients": {token, 0, 5, 5, nil},
"list clients 5-10": {token, 5, 10, 5, nil},
"list last client": {token, 9, 10, 1, nil},
"list empty response": {token, 11, 10, 0, nil},
"list offset < 0": {token, -1, 10, 0, nil},
"list limit < 0": {token, 1, -10, 0, nil},
"list limit = 0": {token, 1, 0, 0, nil},
"list clients with wrong credentials": {wrong, 0, 0, 0, clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
cl, err := svc.ListClients(tc.key, tc.offset, tc.limit)
size := len(cl)
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestRemoveClient(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
cases := map[string]struct {
id string
key string
err error
}{
"remove client with wrong credentials": {client.ID, "?", clients.ErrUnauthorizedAccess},
"remove existing client": {client.ID, token, nil},
"remove removed client": {client.ID, token, nil},
"remove non-existing client": {"?", token, nil},
}
for desc, tc := range cases {
err := svc.RemoveClient(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestCreateChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
cases := map[string]struct {
channel clients.Channel
key string
err error
}{
"create channel": {clients.Channel{}, token, nil},
"create channel with wrong credentials": {clients.Channel{}, wrong, clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.CreateChannel(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestUpdateChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
cases := map[string]struct {
channel clients.Channel
key string
err error
}{
"update existing channel": {channel, token, nil},
"update channel with wrong credentials": {channel, wrong, clients.ErrUnauthorizedAccess},
"update non-existing channel": {clients.Channel{ID: "2", Name: "test"}, token, clients.ErrNotFound},
}
for desc, tc := range cases {
err := svc.UpdateChannel(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestViewChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
cases := map[string]struct {
id string
key string
err error
}{
"view existing channel": {channel.ID, token, nil},
"view channel with wrong credentials": {channel.ID, wrong, clients.ErrUnauthorizedAccess},
"view non-existing channel": {wrong, token, clients.ErrNotFound},
}
for desc, tc := range cases {
_, err := svc.ViewChannel(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestListChannels(t *testing.T) {
svc := newService(map[string]string{token: email})
n := 10
for i := 0; i < n; i++ {
svc.CreateChannel(token, channel)
}
cases := map[string]struct {
key string
offset int
limit int
size int
err error
}{
"list first 5 channels": {token, 0, 5, 5, nil},
"list channels 5-10 channels": {token, 5, 10, 5, nil},
"list last channel": {token, 6, 10, 4, nil},
"list offset < 0": {token, -1, 10, 0, nil},
"list limit < 0": {token, 1, -10, 0, nil},
"list limit = 0": {token, 1, 0, 0, nil},
"list channels with wrong credentials": {wrong, 0, 0, 0, clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
ch, err := svc.ListChannels(tc.key, tc.offset, tc.limit)
size := len(ch)
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestRemoveChannel(t *testing.T) {
svc := newService(map[string]string{token: email})
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
cases := map[string]struct {
id string
key string
err error
}{
"remove channel with wrong credentials": {channel.ID, wrong, clients.ErrUnauthorizedAccess},
"remove existing channel": {channel.ID, token, nil},
"remove removed channel": {channel.ID, token, nil},
"remove non-existing channel": {channel.ID, token, nil},
}
for desc, tc := range cases {
err := svc.RemoveChannel(tc.key, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestConnect(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
cases := map[string]struct {
key string
chanID string
clientID string
err error
}{
"connect client": {token, channel.ID, client.ID, nil},
"connect client with wrong credentials": {wrong, channel.ID, client.ID, clients.ErrUnauthorizedAccess},
"connect client to non-existing channel": {token, wrong, client.ID, clients.ErrNotFound},
}
for desc, tc := range cases {
err := svc.Connect(tc.key, tc.chanID, tc.clientID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
func TestDisconnect(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
svc.Connect(token, chanID, clientID)
cases := []struct {
desc string
key string
chanID string
clientID string
err error
}{
{"disconnect connected client", token, channel.ID, client.ID, nil},
{"disconnect disconnected client", token, channel.ID, client.ID, clients.ErrNotFound},
{"disconnect client with wrong credentials", wrong, channel.ID, client.ID, clients.ErrUnauthorizedAccess},
{"disconnect client from non-existing channel", token, wrong, client.ID, clients.ErrNotFound},
{"disconnect non-existing client", token, channel.ID, wrong, clients.ErrNotFound},
}
for _, tc := range cases {
err := svc.Disconnect(tc.key, tc.chanID, tc.clientID)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestCanAccess(t *testing.T) {
svc := newService(map[string]string{token: email})
clientID, _ := svc.AddClient(token, client)
client.ID = clientID
client.Key = clientID
channel.Clients = []clients.Client{client}
chanID, _ := svc.CreateChannel(token, channel)
channel.ID = chanID
cases := map[string]struct {
key string
channel string
err error
}{
"allowed access": {client.Key, channel.ID, nil},
"not-connected cannot access": {"", channel.ID, clients.ErrUnauthorizedAccess},
"access non-existing channel": {client.Key, wrong, clients.ErrUnauthorizedAccess},
}
for desc, tc := range cases {
_, err := svc.CanAccess(tc.key, tc.channel)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
}
}
+142
View File
@@ -0,0 +1,142 @@
//
// 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/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"
)
const (
sep = ","
defLogLevel = "error"
defPort = "8180"
defCluster = "127.0.0.1"
defKeyspace = "mainflux"
defThingsURL = "localhost:8181"
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"
)
type config struct {
logLevel string
port string
cluster string
keyspace string
thingsURL 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.thingsURL, 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 {
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),
}
}
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(url string, logger logger.Logger) *grpc.ClientConn {
conn, err := grpc.Dial(url, grpc.WithInsecure())
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())
}
+122
View File
@@ -0,0 +1,122 @@
//
// 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",
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()
// Root Commands
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(usersCmd)
rootCmd.AddCommand(thingsCmd)
rootCmd.AddCommand(channelsCmd)
rootCmd.AddCommand(messagesCmd)
// 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)
}
}
-162
View File
@@ -1,162 +0,0 @@
package main
import (
"database/sql"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/clients"
"github.com/mainflux/mainflux/clients/api"
grpcapi "github.com/mainflux/mainflux/clients/api/grpc"
httpapi "github.com/mainflux/mainflux/clients/api/http"
"github.com/mainflux/mainflux/clients/bcrypt"
"github.com/mainflux/mainflux/clients/jwt"
"github.com/mainflux/mainflux/clients/postgres"
log "github.com/mainflux/mainflux/logger"
usersapi "github.com/mainflux/mainflux/users/api/grpc"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
)
const (
defDBHost = "localhost"
defDBPort = "5432"
defDBUser = "mainflux"
defDBPass = "mainflux"
defDBName = "clients"
defHTTPPort = "8180"
defGRPCPort = "8181"
defUsersURL = "localhost:8181"
defSecret = "clients"
envDBHost = "MF_CLIENTS_DB_HOST"
envDBPort = "MF_CLIENTS_DB_PORT"
envDBUser = "MF_CLIENTS_DB_USER"
envDBPass = "MF_CLIENTS_DB_PASS"
envDBName = "MF_CLIENTS_DB"
envHTTPPort = "MF_CLIENTS_HTTP_PORT"
envGRPCPort = "MF_CLIENTS_GRPC_PORT"
envUsersURL = "MF_USERS_URL"
envSecret = "MF_CLIENTS_SECRET"
)
type config struct {
DBHost string
DBPort string
DBUser string
DBPass string
DBName string
HTTPPort string
GRPCPort string
UsersURL string
Secret string
}
func main() {
cfg := loadConfig()
logger := log.New(os.Stdout)
db := connectToDB(cfg, logger)
defer db.Close()
conn := connectToUsersService(cfg.UsersURL, logger)
defer conn.Close()
svc := newService(conn, db, cfg.Secret, logger)
errs := make(chan error, 2)
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
go startGRPCServer(svc, cfg.GRPCPort, 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("Clients service terminated: %s", err))
}
func loadConfig() config {
return 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),
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
GRPCPort: mainflux.Env(envGRPCPort, defGRPCPort),
UsersURL: mainflux.Env(envUsersURL, defUsersURL),
Secret: mainflux.Env(envSecret, defSecret),
}
}
func connectToDB(cfg config, logger log.Logger) *sql.DB {
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)
}
return db
}
func connectToUsersService(usersAddr string, logger log.Logger) *grpc.ClientConn {
conn, err := grpc.Dial(usersAddr, grpc.WithInsecure())
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, secret string, logger log.Logger) clients.Service {
users := usersapi.NewClient(conn)
clientsRepo := postgres.NewClientRepository(db, logger)
channelsRepo := postgres.NewChannelRepository(db, logger)
hasher := bcrypt.New()
idp := jwt.New(secret)
svc := clients.New(users, clientsRepo, channelsRepo, hasher, idp)
svc = api.LoggingMiddleware(svc, logger)
svc = api.MetricsMiddleware(
svc,
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "clients",
Subsystem: "api",
Name: "request_count",
Help: "Number of requests received.",
}, []string{"method"}),
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "clients",
Subsystem: "api",
Name: "request_latency_microseconds",
Help: "Total duration of requests in microseconds.",
}, []string{"method"}),
)
return svc
}
func startHTTPServer(svc clients.Service, port string, logger log.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("Clients service started, exposed port %s", port))
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc))
}
func startGRPCServer(svc clients.Service, port string, logger log.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))
}
server := grpc.NewServer()
mainflux.RegisterClientsServiceServer(server, grpcapi.NewServer(svc))
logger.Info(fmt.Sprintf("Clients gRPC service started, exposed port %s", port))
errs <- server.Serve(listener)
}
-98
View File
@@ -1,98 +0,0 @@
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
"github.com/mainflux/mainflux/coap"
"github.com/mainflux/mainflux/coap/api"
"github.com/mainflux/mainflux/coap/nats"
log "github.com/mainflux/mainflux/logger"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
broker "github.com/nats-io/go-nats"
)
const (
defPort int = 5683
defNatsURL string = broker.DefaultURL
defClientsURL string = "localhost:8181"
envPort string = "MF_COAP_ADAPTER_PORT"
envNatsURL string = "MF_NATS_URL"
envClientsURL string = "MF_CLIENTS_URL"
)
type config struct {
ClientsURL string
NatsURL string
Port int
}
func main() {
cfg := config{
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: defPort,
}
logger := log.New(os.Stdout)
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, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
os.Exit(1)
}
defer conn.Close()
cc := clientsapi.NewClient(conn)
pubsub := nats.New(nc, logger)
svc := coap.New(pubsub)
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() {
p := fmt.Sprintf(":%d", cfg.Port)
logger.Info(fmt.Sprintf("CoAP adapter service started, exposed port %d", cfg.Port))
errs <- api.ListenAndServe(svc, cc, p)
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
err = <-errs
logger.Error(fmt.Sprintf("CoAP adapter terminated: %s", err))
}
+41 -20
View File
@@ -1,7 +1,15 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
@@ -9,39 +17,42 @@ import (
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
adapter "github.com/mainflux/mainflux/http"
"github.com/mainflux/mainflux/http/api"
"github.com/mainflux/mainflux/http/nats"
log "github.com/mainflux/mainflux/logger"
"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
defClientsURL string = "localhost:8181"
envPort string = "MF_HTTP_ADAPTER_PORT"
envNatsURL string = "MF_NATS_URL"
envClientsURL string = "MF_CLIENTS_URL"
defPort string = "8180"
defLogLevel string = "error"
defNatsURL string = broker.DefaultURL
defThingsURL string = "localhost:8181"
envPort string = "MF_HTTP_ADAPTER_PORT"
envLogLevel string = "MF_HTTP_ADAPTER_LOG_LEVEL"
envNatsURL string = "MF_NATS_URL"
envThingsURL string = "MF_THINGS_URL"
)
type config struct {
ClientsURL string
NatsURL string
Port string
ThingsURL string
NatsURL string
LogLevel string
Port string
}
func main() {
cfg := config{
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
Port: mainflux.Env(envPort, defPort),
}
logger := log.New(os.Stdout)
cfg := loadConfig()
logger, err := logger.New(os.Stdout, cfg.LogLevel)
if err != nil {
log.Fatalf(err.Error())
}
nc, err := broker.Connect(cfg.NatsURL)
if err != nil {
@@ -50,14 +61,14 @@ func main() {
}
defer nc.Close()
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
conn, err := grpc.Dial(cfg.ThingsURL, grpc.WithInsecure())
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
os.Exit(1)
}
defer conn.Close()
cc := clientsapi.NewClient(conn)
cc := thingsapi.NewClient(conn)
pub := nats.NewMessagePublisher(nc)
svc := adapter.New(pub)
@@ -95,3 +106,13 @@ func main() {
err = <-errs
logger.Error(fmt.Sprintf("HTTP adapter terminated: %s", err))
}
func loadConfig() config {
return config{
ThingsURL: mainflux.Env(envThingsURL, defThingsURL),
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
LogLevel: mainflux.Env(envLogLevel, defLogLevel),
Port: mainflux.Env(envPort, defPort),
}
}
+148
View File
@@ -0,0 +1,148 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"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"
)
const (
defThingsURL = "localhost:8181"
defLogLevel = "error"
defPort = "8180"
defDBName = "mainflux"
defDBHost = "localhost"
defDBPort = "8086"
defDBUser = "mainflux"
defDBPass = "mainflux"
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"
)
type config struct {
ThingsURL string
LogLevel string
Port 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())
}
conn := connectToThings(cfg.ThingsURL, 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, err := influxdb.New(client, cfg.DBName)
if err != nil {
logger.Error(fmt.Sprintf("Failed to create InfluxDB 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 startHTTPServer(repo, tc, cfg.Port, logger, errs)
err = <-errs
logger.Error(fmt.Sprintf("InfluxDB writer service terminated: %s", err))
}
func loadConfigs() (config, influxdata.HTTPConfig) {
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),
}
clientCfg := influxdata.HTTPConfig{
Addr: fmt.Sprintf("http://%s:%s", cfg.DBHost, cfg.DBPort),
Username: cfg.DBUser,
Password: cfg.DBPass,
}
return cfg, clientCfg
}
func connectToThings(url string, logger logger.Logger) *grpc.ClientConn {
conn, err := grpc.Dial(url, grpc.WithInsecure())
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, logger logger.Logger) readers.MessageRepository {
repo, _ := influxdb.New(client, "mainflux")
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())
}
+141
View File
@@ -0,0 +1,141 @@
//
// 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/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"
)
const (
defThingsURL = "localhost:8181"
defLogLevel = "error"
defPort = "8180"
defDBName = "mainflux"
defDBHost = "localhost"
defDBPort = "27017"
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"
)
type config struct {
thingsURL 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())
}
conn := connectToThings(cfg.thingsURL, 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 {
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),
}
}
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(url string, logger logger.Logger) *grpc.ClientConn {
conn, err := grpc.Dial(url, grpc.WithInsecure())
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, "cassandra-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),
}
}
+204
View File
@@ -0,0 +1,204 @@
//
// 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"
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"
defCacheURL = "localhost:6379"
defCachePass = ""
defCacheDB = "0"
defHTTPPort = "8180"
defGRPCPort = "8181"
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"
envCacheURL = "MF_THINGS_CACHE_URL"
envCachePass = "MF_THINGS_CACHE_PASS"
envCacheDB = "MF_THINGS_CACHE_DB"
envHTTPPort = "MF_THINGS_HTTP_PORT"
envGRPCPort = "MF_THINGS_GRPC_PORT"
envUsersURL = "MF_USERS_URL"
)
type config struct {
LogLevel string
DBHost string
DBPort string
DBUser string
DBPass string
DBName string
CacheURL string
CachePass string
CacheDB string
HTTPPort string
GRPCPort string
UsersURL string
}
func main() {
cfg := loadConfig()
logger, err := logger.New(os.Stdout, cfg.LogLevel)
if err != nil {
log.Fatalf(err.Error())
}
cache := connectToCache(cfg.CacheURL, cfg.CachePass, cfg.CacheDB, logger)
db := connectToDB(cfg, logger)
defer db.Close()
conn := connectToUsersService(cfg.UsersURL, logger)
defer conn.Close()
svc := newService(conn, db, cache, logger)
errs := make(chan error, 2)
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
go startGRPCServer(svc, cfg.GRPCPort, 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 {
return config{
LogLevel: mainflux.Env(envLogLevel, defLogLevel),
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),
CacheURL: mainflux.Env(envCacheURL, defCacheURL),
CachePass: mainflux.Env(envCachePass, defCachePass),
CacheDB: mainflux.Env(envCacheDB, defCacheDB),
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
GRPCPort: mainflux.Env(envGRPCPort, defGRPCPort),
UsersURL: mainflux.Env(envUsersURL, defUsersURL),
}
}
func connectToCache(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(cfg config, logger logger.Logger) *sql.DB {
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)
}
return db
}
func connectToUsersService(usersAddr string, logger logger.Logger) *grpc.ClientConn {
conn, err := grpc.Dial(usersAddr, grpc.WithInsecure())
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, client *redis.Client, logger logger.Logger) things.Service {
users := usersapi.NewClient(conn)
thingsRepo := postgres.NewThingRepository(db, logger)
channelsRepo := postgres.NewChannelRepository(db, logger)
chanCache := rediscache.NewChannelCache(client)
thingCache := rediscache.NewThingCache(client)
idp := uuid.New()
svc := things.New(users, thingsRepo, channelsRepo, chanCache, thingCache, idp)
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, port string, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("Things service started, exposed port %s", port))
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc))
}
func startGRPCServer(svc things.Service, port 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))
}
server := grpc.NewServer()
mainflux.RegisterThingsServiceServer(server, grpcapi.NewServer(svc))
logger.Info(fmt.Sprintf("Things gRPC service started, exposed port %s", port))
errs <- server.Serve(listener)
}
+22 -8
View File
@@ -1,8 +1,16 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"database/sql"
"fmt"
"log"
"net"
"net/http"
"os"
@@ -11,7 +19,7 @@ import (
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/users"
"github.com/mainflux/mainflux/users/api"
grpcapi "github.com/mainflux/mainflux/users/api/grpc"
@@ -24,6 +32,7 @@ import (
)
const (
defLogLevel = "error"
defDBHost = "localhost"
defDBPort = "5432"
defDBUser = "mainflux"
@@ -32,6 +41,7 @@ const (
defHTTPPort = "8180"
defGRPCPort = "8181"
defSecret = "users"
envLogLevel = "MF_USERS_LOG_LEVEL"
envDBHost = "MF_USERS_DB_HOST"
envDBPort = "MF_USERS_DB_PORT"
envDBUser = "MF_USERS_DB_USER"
@@ -43,6 +53,7 @@ const (
)
type config struct {
LogLevel string
DBHost string
DBPort string
DBUser string
@@ -56,8 +67,10 @@ type config struct {
func main() {
cfg := loadConfig()
logger := log.New(os.Stdout)
logger, err := logger.New(os.Stdout, cfg.LogLevel)
if err != nil {
log.Fatalf(err.Error())
}
db := connectToDB(cfg, logger)
defer db.Close()
@@ -73,12 +86,13 @@ func main() {
errs <- fmt.Errorf("%s", <-c)
}()
err := <-errs
err = <-errs
logger.Error(fmt.Sprintf("Users service terminated: %s", err))
}
func loadConfig() config {
return config{
LogLevel: mainflux.Env(envLogLevel, defLogLevel),
DBHost: mainflux.Env(envDBHost, defDBHost),
DBPort: mainflux.Env(envDBPort, defDBPort),
DBUser: mainflux.Env(envDBUser, defDBUser),
@@ -90,7 +104,7 @@ func loadConfig() config {
}
}
func connectToDB(cfg config, logger log.Logger) *sql.DB {
func connectToDB(cfg config, logger logger.Logger) *sql.DB {
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))
@@ -99,7 +113,7 @@ func connectToDB(cfg config, logger log.Logger) *sql.DB {
return db
}
func newService(db *sql.DB, secret string, logger log.Logger) users.Service {
func newService(db *sql.DB, secret string, logger logger.Logger) users.Service {
repo := postgres.New(db)
hasher := bcrypt.New()
idp := jwt.New(secret)
@@ -124,13 +138,13 @@ func newService(db *sql.DB, secret string, logger log.Logger) users.Service {
return svc
}
func startHTTPServer(svc users.Service, port string, logger log.Logger, errs chan error) {
func startHTTPServer(svc users.Service, port string, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
logger.Info(fmt.Sprintf("Users HTTP service started, exposed port %s", port))
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc, logger))
}
func startGRPCServer(svc users.Service, port string, logger log.Logger, errs chan error) {
func startGRPCServer(svc users.Service, port string, logger logger.Logger, errs chan error) {
p := fmt.Sprintf(":%s", port)
listener, err := net.Listen("tcp", p)
if err != nil {
+37 -20
View File
@@ -1,7 +1,15 @@
//
// Copyright (c) 2018
// Mainflux
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
@@ -9,8 +17,8 @@ import (
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
"github.com/mainflux/mainflux"
clientsapi "github.com/mainflux/mainflux/clients/api/grpc"
log "github.com/mainflux/mainflux/logger"
"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"
@@ -20,29 +28,30 @@ import (
)
const (
defPort = "8180"
defNatsURL = broker.DefaultURL
defClientsURL = "localhost:8181"
envPort = "MF_WS_ADAPTER_PORT"
envNatsURL = "MF_NATS_URL"
envClientsURL = "MF_CLIENTS_URL"
defPort = "8180"
defLogLevel = "error"
defNatsURL = broker.DefaultURL
defThingsURL = "localhost:8181"
envPort = "MF_WS_ADAPTER_PORT"
envLogLevel = "MF_WS_ADAPTER_LOG_LEVEL"
envNatsURL = "MF_NATS_URL"
envThingsURL = "MF_THINGS_URL"
)
type config struct {
ClientsURL string
NatsURL string
Port string
ThingsURL string
NatsURL string
LogLevel string
Port string
}
func main() {
cfg := config{
ClientsURL: mainflux.Env(envClientsURL, defClientsURL),
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)
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
@@ -50,14 +59,14 @@ func main() {
}
defer nc.Close()
conn, err := grpc.Dial(cfg.ClientsURL, grpc.WithInsecure())
conn, err := grpc.Dial(cfg.ThingsURL, grpc.WithInsecure())
if err != nil {
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
os.Exit(1)
}
defer conn.Close()
cc := clientsapi.NewClient(conn)
cc := thingsapi.NewClient(conn)
pubsub := nats.New(nc)
svc := adapter.New(pubsub)
svc = api.LoggingMiddleware(svc, logger)
@@ -94,3 +103,11 @@ func main() {
err = <-errs
logger.Error(fmt.Sprintf("WebSocket adapter terminated: %s", err))
}
func loadConfig() config {
return config{
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
LogLevel: mainflux.Env(envLogLevel, defLogLevel),
Port: mainflux.Env(envPort, defPort),
}
}
-59
View File
@@ -1,59 +0,0 @@
# Mainflux CoAP Adapter
Mainflux CoAP adapter provides an [CoAP](http://coap.technology/) API for sending messages through the
platform.
## 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_COAP_ADAPTER_PORT | adapter listening port | `5683` |
| MF_NATS_URL | NATS instance URL | `nats://localhost:4222` |
| MF_CLIENTS_URL | clients service URL | `http://localhost:8181` |
## Deployment
The service 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"
services:
adapter:
image: mainflux/coap-adapter:[version]
container_name: [instance name]
ports:
- [host machine port]:[configured port]
environment:
MF_COAP_ADAPTER_PORT: [Service HTTP port]
MF_NATS_URL: [NATS instance URL]
MF_CLIENTS_URL: [Manager service URL]
```
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
# compile the http
make coap
# copy binary to bin
make install
# set the environment variables and run the service
MF_MANAGER_URL=[Manager service URL] MF_NATS_URL=[NATS instance URL] MF_COAP_ADAPTER_PORT=[Service HTTP port] $GOBIN/mainflux-coap
```
## Usage
Since CoAP protocol does not support `Authorization` header (option), in order to send CoAP messages,
client valid key must be present in `Uri-Query` option.
-160
View File
@@ -1,160 +0,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 (
"errors"
"math"
"sync"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap/nats"
broker "github.com/nats-io/go-nats"
)
const (
responseBackoffMultiplier = 1.5
// AckTimeout is the amount of time to wait for a response.
AckTimeout = int(2 * time.Second)
// MaxRetransmit is the maximum number of times a message will be retransmitted.
MaxRetransmit = 4
)
var (
// 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")
// extracted to avoid recomputation
maxTimeout = int(float64(AckTimeout) * ((math.Pow(2, float64(MaxRetransmit))) - 1) * responseBackoffMultiplier)
)
// Service specifies coap service API.
type Service interface {
mainflux.MessagePublisher
// Subscribes to channel with specified id and adds subscription to
// service map of subscriptions under given ID.
Subscribe(string, string, nats.Channel) error
// Unsubscribe method is used to stop observing resource.
Unsubscribe(string)
// SetTimeout sets timeout to wait CONF messages.
SetTimeout(string, *time.Timer, int) (chan bool, error)
// RemoveTimeout removes timeout when ACK message is received from client
// if timeout existed.
RemoveTimeout(string)
}
var _ Service = (*adapterService)(nil)
type adapterService struct {
pubsub nats.Service
subs map[string]nats.Channel
mu sync.Mutex
}
// New instantiates the CoAP adapter implementation.
func New(pubsub nats.Service) Service {
return &adapterService{
pubsub: pubsub,
subs: make(map[string]nats.Channel),
mu: sync.Mutex{},
}
}
func (svc *adapterService) get(clientID string) (nats.Channel, bool) {
svc.mu.Lock()
obs, ok := svc.subs[clientID]
svc.mu.Unlock()
return obs, ok
}
func (svc *adapterService) put(clientID string, obs nats.Channel) {
svc.mu.Lock()
svc.subs[clientID] = obs
svc.mu.Unlock()
}
func (svc *adapterService) remove(clientID string) {
svc.mu.Lock()
obs, ok := svc.subs[clientID]
if ok {
obs.Closed <- true
delete(svc.subs, clientID)
}
svc.mu.Unlock()
}
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
}
}
return nil
}
func (svc *adapterService) Subscribe(chanID, clientID string, ch nats.Channel) error {
// Remove entry if already exists.
svc.Unsubscribe(clientID)
if err := svc.pubsub.Subscribe(chanID, ch); err != nil {
return ErrFailedSubscription
}
svc.put(clientID, ch)
return nil
}
func (svc *adapterService) Unsubscribe(clientID string) {
svc.remove(clientID)
}
func (svc *adapterService) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
sub, ok := svc.get(clientID)
if !ok {
return nil, errors.New("observer entry not found")
}
go func() {
for {
select {
case _, ok := <-sub.Timer:
timer.Stop()
if ok {
sub.Notify <- false
}
return
case <-timer.C:
duration *= 2
if duration >= maxTimeout {
timer.Stop()
sub.Notify <- false
svc.Unsubscribe(clientID)
return
}
timer.Reset(time.Duration(duration))
sub.Notify <- true
}
}
}()
return sub.Notify, nil
}
func (svc *adapterService) RemoveTimeout(clientID string) {
if sub, ok := svc.get(clientID); ok {
sub.Timer <- false
}
}
-3
View File
@@ -1,3 +0,0 @@
// Package api contains API-related concerns: endpoint definitions, middlewares
// and all resource representations.
package api
-66
View File
@@ -1,66 +0,0 @@
// +build !test
package api
import (
"fmt"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap"
"github.com/mainflux/mainflux/coap/nats"
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) {
message := fmt.Sprintf("Method publish to channel %s took %s to complete", msg.Channel, 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, clientID string, channel nats.Channel) (err error) {
defer func(begin time.Time) {
message := fmt.Sprintf("Method subscribe to channel %s took %s to complete", chanID, 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, clientID, channel)
}
func (lm *loggingMiddleware) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
return lm.svc.SetTimeout(clientID, timer, duration)
}
func (lm *loggingMiddleware) RemoveTimeout(clientID string) {
lm.svc.RemoveTimeout(clientID)
}
func (lm *loggingMiddleware) Unsubscribe(clientID string) {
defer func(begin time.Time) {
lm.logger.Info(fmt.Sprintf("Method unsubscribe for client %s took %s to complete", clientID, time.Since(begin)))
}(time.Now())
lm.svc.Unsubscribe(clientID)
}
-54
View File
@@ -1,54 +0,0 @@
// +build !test
package api
import (
"time"
"github.com/go-kit/kit/metrics"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap"
"github.com/mainflux/mainflux/coap/nats"
)
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, clientID string, channel nats.Channel) error {
return mm.svc.Subscribe(chanID, clientID, channel)
}
func (mm *metricsMiddleware) SetTimeout(clientID string, timer *time.Timer, duration int) (chan bool, error) {
return mm.svc.SetTimeout(clientID, timer, duration)
}
func (mm *metricsMiddleware) RemoveTimeout(clientID string) {
mm.svc.RemoveTimeout(clientID)
}
func (mm *metricsMiddleware) Unsubscribe(clientID string) {
mm.svc.Unsubscribe(clientID)
}
-150
View File
@@ -1,150 +0,0 @@
package api
import (
"context"
"fmt"
"net"
"strings"
"time"
"google.golang.org/grpc/codes"
"github.com/asaskevich/govalidator"
mux "github.com/dereulenspiegel/coap-mux"
gocoap "github.com/dustin/go-coap"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/coap"
"google.golang.org/grpc/status"
)
const (
chanID = "id"
keyHeader = "key"
)
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]) != keyHeader {
return "", errBadOption
}
return arr[1], nil
}
func authorize(msg *gocoap.Message, res *gocoap.Message, cid string) (publisher *mainflux.Identity, err error) {
if !govalidator.IsUUID(cid) {
res.Code = gocoap.NotFound
return
}
// 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
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
publisher, 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
}
res.Code = gocoap.InternalServerError
}
return
}
func serve(svc coap.Service, conn *net.UDPConn, data []byte, addr *net.UDPAddr, rh gocoap.Handler) {
msg, err := gocoap.ParseMessage(data)
if err != nil {
return
}
res := &gocoap.Message{
Type: gocoap.NonConfirmable,
Code: gocoap.Content,
MessageID: msg.MessageID,
Token: msg.Token,
Payload: []byte{},
}
switch msg.Type {
case gocoap.Reset:
if len(msg.Payload) != 0 {
res.Code = gocoap.BadRequest
break
}
cid := mux.Var(&msg, chanID)
res.Type = gocoap.Acknowledgement
publisher, err := authorize(&msg, res, cid)
if err != nil {
break
}
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
svc.RemoveTimeout(id)
svc.Unsubscribe(id)
case gocoap.Acknowledgement:
cid := mux.Var(&msg, chanID)
res.Type = gocoap.Acknowledgement
publisher, err := authorize(&msg, res, cid)
if err != nil {
break
}
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
svc.RemoveTimeout(id)
default:
res = rh.ServeCOAP(conn, addr, &msg)
}
if res != nil && msg.IsConfirmable() {
gocoap.Transmit(conn, addr, *res)
}
}
// ListenAndServe binds to the given address and serve requests forever.
func ListenAndServe(svc coap.Service, csc mainflux.ClientsServiceClient, addr string) error {
handler := makeHandler(addr, svc)
auth = csc
uaddr, err := net.ResolveUDPAddr(network, addr)
if err != nil {
return err
}
conn, err := net.ListenUDP(network, uaddr)
if err != nil {
return err
}
buf := make([]byte, maxPktLen)
for {
nr, addr, err := conn.ReadFromUDP(buf)
if err != nil {
if neterr, ok := err.(net.Error); ok && (neterr.Temporary() || neterr.Timeout()) {
time.Sleep(5 * time.Millisecond)
continue
}
return err
}
tmp := make([]byte, nr)
copy(tmp, buf)
go serve(svc, conn, tmp, addr, handler)
}
}
-225
View File
@@ -1,225 +0,0 @@
package api
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/go-zoo/bone"
"github.com/mainflux/mainflux/coap"
"github.com/mainflux/mainflux/coap/nats"
"math/rand"
mux "github.com/dereulenspiegel/coap-mux"
gocoap "github.com/dustin/go-coap"
"github.com/mainflux/mainflux"
)
var (
errBadRequest = errors.New("bad request")
errBadOption = errors.New("bad option")
auth mainflux.ClientsServiceClient
)
const (
maxPktLen = 1500
network = "udp"
protocol = "coap"
)
const (
// Approximately number of supported requests per second
timestamp = int64(time.Millisecond) * 31
)
type handler func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message
// NotFoundHandler handles erroneously formed requests.
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
}
func version(port string) {
b := bone.New()
b.GetFunc("/version", mainflux.Version("CoAP"))
http.ListenAndServe(port, b)
}
func makeHandler(port string, svc coap.Service) gocoap.Handler {
go version(port)
r := mux.NewRouter()
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(receive(svc))).Methods(gocoap.POST)
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(observe(svc))).Methods(gocoap.GET)
r.NotFoundHandler = gocoap.FuncHandler(NotFoundHandler)
return r
}
func receive(svc coap.Service) handler {
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
var res *gocoap.Message
if msg.IsConfirmable() {
res = &gocoap.Message{
Type: gocoap.Acknowledgement,
Code: gocoap.Content,
MessageID: msg.MessageID,
Token: msg.Token,
Payload: []byte{},
}
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
}
if len(msg.Payload) == 0 && msg.IsConfirmable() {
res.Code = gocoap.BadRequest
return res
}
cid := mux.Var(msg, "id")
publisher, err := authorize(msg, res, cid)
if err != nil {
return res
}
rawMsg := mainflux.RawMessage{
Channel: cid,
Publisher: publisher.GetValue(),
Protocol: protocol,
Payload: msg.Payload,
}
if err := svc.Publish(rawMsg); err != nil {
res.Code = gocoap.InternalServerError
}
return res
}
}
func observe(svc coap.Service) handler {
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
var res *gocoap.Message
if msg.IsConfirmable() {
res = &gocoap.Message{
Type: gocoap.Acknowledgement,
Code: gocoap.Content,
MessageID: msg.MessageID,
Token: msg.Token,
Payload: []byte{},
}
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
}
cid := mux.Var(msg, "id")
publisher, err := authorize(msg, res, cid)
if err != nil {
return res
}
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 1 {
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
svc.Unsubscribe(id)
}
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 0 {
ch := nats.Channel{
Messages: make(chan mainflux.RawMessage),
Closed: make(chan bool),
Timer: make(chan bool),
Notify: make(chan bool),
}
id := fmt.Sprintf("%s-%x", publisher, msg.Token)
if err := svc.Subscribe(cid, id, ch); err != nil {
res.Code = gocoap.InternalServerError
return res
}
go handleSub(svc, id, conn, addr, msg, ch)
res.AddOption(gocoap.Observe, 0)
}
return res
}
}
func sendMessage(svc coap.Service, id string, conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) error {
buff := new(bytes.Buffer)
now := time.Now().UnixNano() / timestamp
if err := binary.Write(buff, binary.BigEndian, now); err != nil {
return err
}
observeVal := buff.Bytes()
msg.SetOption(gocoap.Observe, observeVal[len(observeVal)-3:])
if msg.IsConfirmable() {
timer := time.NewTimer(time.Duration(coap.AckTimeout))
ch, err := svc.SetTimeout(id, timer, coap.AckTimeout)
if err != nil {
return err
}
go sendConfirmable(conn, addr, msg, ch)
return nil
}
return gocoap.Transmit(conn, addr, *msg)
}
func sendConfirmable(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message, ch chan bool) {
msg.SetOption(gocoap.MaxRetransmit, coap.MaxRetransmit)
// Try to transmit MAX_RETRANSMITION times; every attempt duplicates timeout between transmission.
for i := 0; i < coap.MaxRetransmit; i++ {
if err := gocoap.Transmit(conn, addr, *msg); err != nil {
return
}
state, ok := <-ch
if !state || !ok {
return
}
}
return
}
func handleSub(svc coap.Service, id string, conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message, ch nats.Channel) {
// According to RFC (https://tools.ietf.org/html/rfc7641#page-18), CON message must be sent at least every
// 24 hours. Since 24 hours is too long for our purposes, we use 12.
ticker := time.NewTicker(12 * time.Hour)
res := &gocoap.Message{
Type: gocoap.NonConfirmable,
Code: gocoap.Content,
MessageID: msg.MessageID,
Token: msg.Token,
Payload: []byte{},
}
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
res.SetOption(gocoap.LocationPath, msg.Path())
for {
select {
case <-ticker.C:
ticker.Stop()
res.Type = gocoap.Confirmable
rand.Seed(time.Now().UnixNano())
if err := sendMessage(svc, id, conn, addr, res); err != nil {
ticker.Stop()
return
}
case rawMsg, ok := <-ch.Messages:
if !ok {
ticker.Stop()
return
}
res.Type = gocoap.NonConfirmable
res.Payload = rawMsg.Payload
if err := sendMessage(svc, id, conn, addr, res); err != nil {
ticker.Stop()
return
}
}
}
}
-53
View File
@@ -1,53 +0,0 @@
// Package nats contains NATS-specific message repository implementation.
package nats
import (
"fmt"
"github.com/golang/protobuf/proto"
"github.com/mainflux/mainflux"
log "github.com/mainflux/mainflux/logger"
broker "github.com/nats-io/go-nats"
)
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
const prefix = "channel"
type natsPublisher struct {
nc *broker.Conn
logger log.Logger
}
// New instantiates NATS message pubsub.
func New(nc *broker.Conn, l log.Logger) Service {
return &natsPublisher{nc, l}
}
func (pubsub *natsPublisher) Publish(msg mainflux.RawMessage) error {
data, err := proto.Marshal(&msg)
if err != nil {
return err
}
return pubsub.nc.Publish(fmt.Sprintf("%s.%s", prefix, msg.Channel), data)
}
func (pubsub *natsPublisher) Subscribe(chanID string, channel Channel) error {
sub, err := pubsub.nc.Subscribe(fmt.Sprintf("%s.%s", prefix, chanID), func(msg *broker.Msg) {
if msg == nil {
return
}
var rawMsg mainflux.RawMessage
if err := proto.Unmarshal(msg.Data, &rawMsg); err != nil {
return
}
channel.Messages <- rawMsg
})
go func() {
<-channel.Closed
sub.Unsubscribe()
channel.Close()
}()
return err
}
-26
View File
@@ -1,26 +0,0 @@
package nats
import "github.com/mainflux/mainflux"
// Service specifies NATS service API.
type Service interface {
mainflux.MessagePublisher
// Subscribe is used to subscribe to channel with specified id.
Subscribe(string, Channel) error
}
// Channel is used for receiving and sending messages.
type Channel struct {
Messages chan mainflux.RawMessage
Closed chan bool
Timer chan bool
Notify chan bool
}
// Close channel and stop message transfer.
func (channel Channel) Close() {
close(channel.Messages)
close(channel.Closed)
close(channel.Timer)
close(channel.Notify)
}
+1 -1
View File
@@ -9,7 +9,7 @@
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
"favicon.png"
],
"index": "index.html",
"main": "main.ts",
+2 -2
View File
@@ -1,5 +1,5 @@
# Stage 0, based on Node.js, to build and compile Angular
FROM node:8.6 as node
FROM node:9.5 as node
WORKDIR /app
COPY package.json /app/
RUN npm install
@@ -8,6 +8,6 @@ ARG env=prod
RUN npm run build -- --prod --environment $env
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
FROM nginx:1.13
FROM nginx:1.13-alpine
COPY --from=node /app/dist/ /usr/share/nginx/html
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
+1 -1
View File
@@ -1,5 +1,5 @@
server {
listen 80;
listen 3000;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
+10 -3
View File
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { Route, RouterModule } from '@angular/router';
@@ -6,13 +13,13 @@ import { ProtectedGuard, PublicGuard } from 'ngx-auth';
import { LoginComponent } from './components/auth/login/login.component';
import { SignupComponent } from './components/auth/signup/signup.component';
import { ChannelsComponent } from './components/channels/channels.component';
import { ClientsComponent } from './components/clients/clients.component';
import { ThingsComponent } from './components/things/things.component';
const routes: Route[] = [
{ path: '', redirectTo: 'clients', pathMatch: 'full'},
{ path: '', redirectTo: 'things', pathMatch: 'full'},
{ path: 'login', component: LoginComponent, canActivate: [PublicGuard]},
{ path: 'signup', component: SignupComponent, canActivate: [PublicGuard]},
{ path: 'clients', component: ClientsComponent, canActivate: [ProtectedGuard]},
{ path: 'things', component: ThingsComponent, canActivate: [ProtectedGuard]},
{ path: 'channels', component: ChannelsComponent, canActivate: [ProtectedGuard]}
];
+9 -2
View File
@@ -1,3 +1,10 @@
<!--
Copyright (c) 2018
Mainflux
SPDX-License-Identifier: Apache-2.0
-->
<div *mobxAutorun>
<div class="loading" *ngIf="uiStore.loading" fxLayoutAlign="center center">
<mat-progress-spinner [mode]="'indeterminate'"></mat-progress-spinner>
@@ -5,11 +12,11 @@
<mat-toolbar color="primary" class="mat-elevation-z6">
<h1 fxFlex>Mainflux</h1>
<ng-container *ngIf="authStore.isAuthenticated">
<a [routerLink]="['/clients']" mat-button>Clients</a>
<a [routerLink]="['/things']" mat-button>Things</a>
<a [routerLink]="['/channels']" mat-button>Channels</a>
<a (click)="logout()" mat-button>Logout</a>
</ng-container>
</mat-toolbar>
<router-outlet></router-outlet>
</div>
+7
View File
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
.loading {
position: absolute;
width: 100%;
+9 -2
View File
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { HttpClientModule } from '@angular/common/http';
import { async, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
@@ -7,7 +14,7 @@ import { MaterialModule } from './core/material/material.module';
import { AuthenticationService } from './core/services/auth/authentication.service';
import { TokenStorage } from './core/services/auth/token-storage.service';
import { ChannelsService } from './core/services/channels/channels.service';
import { ClientsService } from './core/services/clients/clients.service';
import { ThingsService } from './core/services/things/things.service';
import { UiStore } from './core/store/ui.store';
import { AuthStore } from './core/store/auth.store';
@@ -27,7 +34,7 @@ describe('AppComponent', () => {
AuthStore,
AuthenticationService,
TokenStorage,
ClientsService,
ThingsService,
ChannelsService,
]
}).compileComponents();
+7
View File
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { reaction } from 'mobx';
+23 -14
View File
@@ -1,7 +1,15 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import './rxjs-extensions.ts';
import 'hammerjs';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { APP_BASE_HREF } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
@@ -14,21 +22,21 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './components/auth/login/login.component';
import { SignupComponent } from './components/auth/signup/signup.component';
import { AddChannelDialogComponent } from './components/channels/add-channel-dialog/add-channel-dialog.component';
import { ChannelDialogComponent } from './components/channels/channel-dialog/channel-dialog.component';
import { ChannelsComponent } from './components/channels/channels.component';
import { AddClientDialogComponent } from './components/clients/add-client-dialog/add-client-dialog.component';
import { ClientsComponent } from './components/clients/clients.component';
import { ThingDialogComponent } from './components/things/thing-dialog/thing-dialog.component';
import { ThingsComponent } from './components/things/things.component';
import { ConfirmationDialogComponent } from './components/shared/confirmation-dialog/confirmation-dialog.component';
import { MaterialModule } from './core/material/material.module';
import { AuthenticationService } from './core/services/auth/authentication.service';
import { TokenStorage } from './core/services/auth/token-storage.service';
import { ChannelsService } from './core/services/channels/channels.service';
import { ClientsService } from './core/services/clients/clients.service';
import { ThingsService } from './core/services/things/things.service';
import { MockAuthService } from './core/services/mock-auth.service';
import { MockChannelsService } from './core/services/mock-channels.service';
import { MockClientsService } from './core/services/mock-clients.service';
import { MockThingsService } from './core/services/mock-things.service';
import { ChannelsStore } from './core/store/channels.store';
import { ClientsStore } from './core/store/clients.store';
import { ThingsStore } from './core/store/things.store';
import { UiStore } from './core/store/ui.store';
import { AuthStore } from './core/store/auth.store';
import { UnauthorizedInterceptor } from './core/services/auth/unauthorized.interceptor';
@@ -40,13 +48,13 @@ export function factory(authenticationService: AuthenticationService) {
@NgModule({
declarations: [
AppComponent,
ClientsComponent,
ThingsComponent,
ChannelsComponent,
SignupComponent,
LoginComponent,
AddClientDialogComponent,
ThingDialogComponent,
ConfirmationDialogComponent,
AddChannelDialogComponent,
ChannelDialogComponent,
],
imports: [
AuthModule,
@@ -61,13 +69,13 @@ export function factory(authenticationService: AuthenticationService) {
],
providers: [
UiStore,
ClientsStore,
ThingsStore,
ChannelsStore,
AuthStore,
MockAuthService,
MockClientsService,
MockThingsService,
MockChannelsService,
ClientsService,
ThingsService,
ChannelsService,
TokenStorage,
AuthenticationService,
@@ -79,11 +87,12 @@ export function factory(authenticationService: AuthenticationService) {
useFactory: factory
},
{ provide: HTTP_INTERCEPTORS, useClass: UnauthorizedInterceptor, multi: true },
{ provide: APP_BASE_HREF, useValue: '/app/' }
],
bootstrap: [AppComponent],
entryComponents: [
AddClientDialogComponent,
AddChannelDialogComponent,
ThingDialogComponent,
ChannelDialogComponent,
ConfirmationDialogComponent
]
})
@@ -1,3 +1,10 @@
<!--
Copyright (c) 2018
Mainflux
SPDX-License-Identifier: Apache-2.0
-->
<div class="container" style="height: 80vh" fxLayoutAlign="center center">
<form fxLayout="column" [formGroup]="loginForm" (ngSubmit)="login()" fxFlex="30%" fxFlex.sm="70%" fxFlex.xs="90%">
<mat-card>
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
::ng-deep mat-card-content.loginCard {
height: 200px;
}
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@@ -1,3 +1,10 @@
<!--
Copyright (c) 2018
Mainflux
SPDX-License-Identifier: Apache-2.0
-->
<div class="container" style="height: 80vh" fxLayoutAlign="center center">
<form fxLayout="column" [formGroup]="signupForm" (ngSubmit)="signup()" fxFlex="30%" fxFlex.sm="70%" fxFlex.xs="90%">
<mat-card>
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
::ng-deep mat-card-content.loginCard {
height: 200px;
}
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
@@ -1,3 +1,10 @@
<!--
Copyright (c) 2018
Mainflux
SPDX-License-Identifier: Apache-2.0
-->
<form [formGroup]="addChannelForm" (ngSubmit)="onAddChannel()">
<h1 *ngIf="!editMode" mat-dialog-title>Add channel</h1>
<h1 *ngIf="editMode" mat-dialog-title>Edit channel</h1>
@@ -10,8 +17,8 @@
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="Connected clients" formControlName="connected" [compareWith]="compareFunction" multiple>
<mat-option *ngFor="let client of clientsStore.clients" [value]="client">{{client.name}}</mat-option>
<mat-select placeholder="Connected things" formControlName="connected" [compareWith]="compareFunction" multiple>
<mat-option *ngFor="let thing of thingsStore.things" [value]="thing">{{thing.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>
@@ -19,4 +26,4 @@
<button mat-button mat-dialog-close>Cancel</button>
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addChannelForm.invalid">Ok</button>
</div>
</form>
</form>
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -9,18 +16,18 @@ import { MaterialModule } from '../../../core/material/material.module';
import { AuthenticationService } from '../../../core/services/auth/authentication.service';
import { TokenStorage } from '../../../core/services/auth/token-storage.service';
import { ChannelsService } from '../../../core/services/channels/channels.service';
import { ClientsService } from '../../../core/services/clients/clients.service';
import { AddChannelDialogComponent } from './add-channel-dialog.component';
import { ClientsStore } from '../../../core/store/clients.store';
import { ThingsService } from '../../../core/services/things/things.service';
import { ChannelDialogComponent } from './channel-dialog.component';
import { ThingsStore } from '../../../core/store/things.store';
import { UiStore } from '../../../core/store/ui.store';
describe('AddChannelDialogComponent', () => {
let component: AddChannelDialogComponent;
let fixture: ComponentFixture<AddChannelDialogComponent>;
describe('ChannelDialogComponent', () => {
let component: ChannelDialogComponent;
let fixture: ComponentFixture<ChannelDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddChannelDialogComponent ],
declarations: [ ChannelDialogComponent ],
imports: [
MaterialModule,
MatDialogModule,
@@ -31,11 +38,11 @@ describe('AddChannelDialogComponent', () => {
NoopAnimationsModule
],
providers: [
ClientsStore,
ThingsStore,
UiStore,
AuthenticationService,
TokenStorage,
ClientsService,
ThingsService,
ChannelsService,
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: [] },
@@ -45,7 +52,7 @@ describe('AddChannelDialogComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddChannelDialogComponent);
fixture = TestBed.createComponent(ChannelDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -1,26 +1,33 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { toJS } from 'mobx';
import { ClientsStore } from '../../../core/store/clients.store';
import { Channel, Client } from '../../../core/store/models';
import { ThingsStore } from '../../../core/store/things.store';
import { Channel, Thing } from '../../../core/store/models';
@Component({
selector: 'app-add-channel-dialog',
templateUrl: './add-channel-dialog.component.html',
styleUrls: ['./add-channel-dialog.component.scss']
selector: 'app-channel-dialog',
templateUrl: './channel-dialog.component.html',
styleUrls: ['./channel-dialog.component.scss']
})
export class AddChannelDialogComponent implements OnInit {
export class ChannelDialogComponent implements OnInit {
addChannelForm: FormGroup;
@Output() submit: EventEmitter<Channel> = new EventEmitter<Channel>();
editMode: boolean;
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<AddChannelDialogComponent>,
private dialogRef: MatDialogRef<ChannelDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: Channel,
public clientsStore: ClientsStore,
public thingsStore: ThingsStore,
) { }
ngOnInit() {
@@ -46,7 +53,7 @@ export class AddChannelDialogComponent implements OnInit {
this.dialogRef.close();
}
compareFunction(obj1: Client, obj2: Client) {
compareFunction(obj1: Thing, obj2: Thing) {
return obj1.id === obj2.id;
}
}
@@ -1,3 +1,10 @@
<!--
Copyright (c) 2018
Mainflux
SPDX-License-Identifier: Apache-2.0
-->
<div class="channelsContainer" fxLayout="row" fxLayoutWrap>
<ng-container [ngSwitch]="channelsStore.channels?.length > 0">
<ng-container *ngSwitchCase="true">
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
.addButton {
position: fixed;
bottom: 12px;
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -9,9 +16,9 @@ import { MaterialModule } from '../../core/material/material.module';
import { AuthenticationService } from '../../core/services/auth/authentication.service';
import { TokenStorage } from '../../core/services/auth/token-storage.service';
import { ChannelsService } from '../../core/services/channels/channels.service';
import { ClientsService } from '../../core/services/clients/clients.service';
import { ThingsService } from '../../core/services/things/things.service';
import { ChannelsComponent } from './channels.component';
import { ClientsStore } from '../../core/store/clients.store';
import { ThingsStore } from '../../core/store/things.store';
import { UiStore } from '../../core/store/ui.store';
import { ChannelsStore } from '../../core/store/channels.store';
@@ -33,9 +40,9 @@ describe('ChannelsComponent', () => {
],
providers: [
{
provide: ClientsStore,
provide: ThingsStore,
useClass: class {
getClients = jasmine.createSpy('getClients');
getThings = jasmine.createSpy('getThings');
}
},
UiStore,
@@ -47,7 +54,7 @@ describe('ChannelsComponent', () => {
},
AuthenticationService,
TokenStorage,
ClientsService,
ThingsService,
ChannelsService,
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: [] },
@@ -1,12 +1,19 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material';
import { Observable } from 'rxjs/Observable';
import { ChannelsStore } from '../../core/store/channels.store';
import { ClientsStore } from '../../core/store/clients.store';
import { ThingsStore } from '../../core/store/things.store';
import { Channel } from '../../core/store/models';
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
import { AddChannelDialogComponent } from './add-channel-dialog/add-channel-dialog.component';
import { ChannelDialogComponent } from './channel-dialog/channel-dialog.component';
@Component({
selector: 'app-channels',
@@ -19,17 +26,17 @@ export class ChannelsComponent implements OnInit {
constructor(
private dialog: MatDialog,
public clientsStore: ClientsStore,
public thingsStore: ThingsStore,
public channelsStore: ChannelsStore,
) { }
ngOnInit() {
this.channelsStore.getChannels();
this.clientsStore.getClients();
this.thingsStore.getThings();
}
addChannel() {
const dialogRef = this.dialog.open(AddChannelDialogComponent);
const dialogRef = this.dialog.open(ChannelDialogComponent);
dialogRef.componentInstance.submit.subscribe((channel: Channel) => {
this.channelsStore.addChannel(channel);
@@ -37,7 +44,7 @@ export class ChannelsComponent implements OnInit {
}
editChannel(channel: Channel) {
const dialogRef = this.dialog.open(AddChannelDialogComponent, {
const dialogRef = this.dialog.open(ChannelDialogComponent, {
data: channel
});
@@ -1,43 +0,0 @@
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Client } from '../../../core/store/models';
@Component({
selector: 'app-add-client-dialog',
templateUrl: './add-client-dialog.component.html',
styleUrls: ['./add-client-dialog.component.scss']
})
export class AddClientDialogComponent implements OnInit {
addClientForm: FormGroup;
@Output() submit: EventEmitter<Client> = new EventEmitter<Client>();
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<AddClientDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: Client
) { }
ngOnInit() {
this.addClientForm = this.fb.group(
{
id: [''],
type: ['', [Validators.required]],
name: ['', [Validators.required, Validators.minLength(5)]],
payload: ['']
}
);
if (this.data) {
this.addClientForm.patchValue(this.data);
this.addClientForm.get('payload').patchValue(JSON.stringify(this.data.payload));
}
}
onAddClient() {
const client = this.addClientForm.value;
this.submit.emit(client);
this.dialogRef.close();
}
}
@@ -1,63 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material';
import { toJS } from 'mobx';
import { Observable } from 'rxjs/Observable';
import { Client } from '../../core/store/models';
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
import { AddClientDialogComponent } from './add-client-dialog/add-client-dialog.component';
import { ClientsStore } from '../../core/store/clients.store';
import { ChannelsStore } from '../../core/store/channels.store';
@Component({
selector: 'app-clients',
templateUrl: './clients.component.html',
styleUrls: ['./clients.component.scss']
})
export class ClientsComponent implements OnInit {
clients: Observable<Client[]>;
displayedColumns = ['id', 'name', 'type', 'payload', 'actions'];
constructor(
private dialog: MatDialog,
public clientsStore: ClientsStore,
public channelsStore: ChannelsStore,
) { }
ngOnInit() {
this.clientsStore.getClients();
this.channelsStore.getChannels();
}
addClient() {
const dialogRef = this.dialog.open(AddClientDialogComponent);
dialogRef.componentInstance.submit.subscribe((client: Client) => {
this.clientsStore.addClient(client);
});
}
editClient(client: Client) {
const dialogRef = this.dialog.open(AddClientDialogComponent, {
data: client
});
dialogRef.componentInstance.submit.subscribe((editedClient: Client) => {
this.clientsStore.editClient(toJS(editedClient));
});
}
deleteClient(client: Client) {
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: {
question: 'Are you sure you want to delete the client?'
}
});
dialogRef.afterClosed().subscribe((result) => {
if (result) {
this.clientsStore.deleteClient(toJS(client));
}
});
}
}
@@ -1,3 +1,10 @@
<!--
Copyright (c) 2018
Mainflux
SPDX-License-Identifier: Apache-2.0
-->
<h1 mat-dialog-title>{{data.question}}</h1>
<div mat-dialog-actions fxLayoutAlign="end end">
<button mat-button mat-dialog-close>Cancel</button>
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
@@ -1,3 +1,10 @@
/*
* Copyright (c) 2018
* Mainflux
*
* SPDX-License-Identifier: Apache-2.0
*/
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material';
@@ -1,9 +1,9 @@
<form [formGroup]="addClientForm" (ngSubmit)="onAddClient()">
<h1 mat-dialog-title>Add client</h1>
<form [formGroup]="addThingForm" (ngSubmit)="onAddThing()">
<h1 mat-dialog-title>Add thing</h1>
<div mat-dialog-content fxLayout="column">
<input type="hidden" formControlName="id">
<mat-form-field>
<mat-select placeholder="Client type" formControlName="type">
<mat-form-field *ngIf="!editMode">
<mat-select placeholder="Thing type" formControlName="type">
<mat-option value="app">
App
</mat-option>
@@ -11,24 +11,24 @@
Device
</mat-option>
</mat-select>
<mat-error *ngIf="addClientForm.get('type').errors?.required">
<mat-error *ngIf="addThingForm.get('type').errors?.required">
Type is required
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput tabindex="1" placeholder="Name" formControlName="name" />
<mat-error *ngIf="addClientForm.get('name').errors?.required">
<mat-error *ngIf="addThingForm.get('name').errors?.required">
Name is required
</mat-error>
</mat-form-field>
<mat-form-field>
<textarea formControlName="payload" matInput placeholder="Payload" matTextareaAutosize matAutosizeMinRows="2"
<textarea formControlName="metadata" matInput placeholder="Metadata" matTextareaAutosize matAutosizeMinRows="2"
matAutosizeMaxRows="5"></textarea>
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-button mat-dialog-close>Cancel</button>
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addClientForm.invalid">Ok</button>
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addThingForm.invalid">Ok</button>
</div>
</form>
</form>
@@ -9,16 +9,16 @@ import { MaterialModule } from '../../../core/material/material.module';
import { AuthenticationService } from '../../../core/services/auth/authentication.service';
import { TokenStorage } from '../../../core/services/auth/token-storage.service';
import { ChannelsService } from '../../../core/services/channels/channels.service';
import { ClientsService } from '../../../core/services/clients/clients.service';
import { AddClientDialogComponent } from './add-client-dialog.component';
import { ThingsService } from '../../../core/services/things/things.service';
import { ThingDialogComponent } from './thing-dialog.component';
describe('AddClientDialogComponent', () => {
let component: AddClientDialogComponent;
let fixture: ComponentFixture<AddClientDialogComponent>;
describe('ThingDialogComponent', () => {
let component: ThingDialogComponent;
let fixture: ComponentFixture<ThingDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddClientDialogComponent ],
declarations: [ ThingDialogComponent ],
imports: [
MaterialModule,
MatDialogModule,
@@ -31,7 +31,7 @@ describe('AddClientDialogComponent', () => {
providers: [
AuthenticationService,
TokenStorage,
ClientsService,
ThingsService,
ChannelsService,
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: [] },
@@ -41,7 +41,7 @@ describe('AddClientDialogComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddClientDialogComponent);
fixture = TestBed.createComponent(ThingDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

Some files were not shown because too many files have changed in this diff Show More