mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 07:14:46 +00:00
Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea79f22269 | |||
| 767c900c48 | |||
| d504c4f67d | |||
| e95e010a3a | |||
| a09e3e214a | |||
| 7c4c6109f4 | |||
| 8c3bff5f4b | |||
| 2ed1471d5f | |||
| 61b2d6b87b | |||
| 2f09701028 | |||
| a0ed73d09b | |||
| 74dfd69767 | |||
| b2ce9f5597 | |||
| 3255f32a9c | |||
| dbd8f9be23 | |||
| 3300814026 | |||
| a60a5c1ba1 | |||
| fa7d638453 | |||
| 1c09632671 | |||
| 06921fbee6 | |||
| b69d31817d | |||
| 225f56fc3e | |||
| 133383ecb7 | |||
| 2ee0ed18e0 | |||
| c418a9a4cf | |||
| 351d6e7bfb | |||
| 434a57d8e1 | |||
| 94574e3707 | |||
| b12bb3374b | |||
| 3f3b42b511 | |||
| 1df4dcd7b7 | |||
| 8966a13760 | |||
| 0fbb21c3fb | |||
| 998c7ef9ef | |||
| dc16eb5451 | |||
| 53036b5ae7 | |||
| c04909d645 | |||
| bf9e148120 | |||
| 330545ad38 | |||
| 1947d39119 | |||
| 0b770ce49e | |||
| 2c6379f823 | |||
| c8e7a97122 | |||
| 6bc287e028 | |||
| f9f25e2d2f | |||
| ca6968f8d0 | |||
| a618627599 | |||
| fedbe1c422 | |||
| 7ff738360b | |||
| a0505e0eeb | |||
| 3bbd21f583 | |||
| c788ef64c0 | |||
| 22fc26b375 | |||
| 8dbe2f4066 | |||
| 72fa9db939 | |||
| 7a3d7fa8ec | |||
| ea1993665d | |||
| bd00c5fa13 | |||
| c75aaa9d45 | |||
| cdd2eb4bc7 | |||
| 584c8591ad | |||
| 643dc12408 | |||
| 655141e941 | |||
| c8069827bb | |||
| 1e017bdbcc | |||
| 5f6bbf4b0a | |||
| 49a41d909e | |||
| d1012d14c7 | |||
| 8a2734d122 | |||
| 399cc051b3 | |||
| 179471d4d7 | |||
| 607a0b22e0 | |||
| f354332fa1 | |||
| 9725c2723d | |||
| 61eed0c72d | |||
| 715a662f0d | |||
| b9bf63e377 | |||
| 21ab52576c | |||
| 7db3b295be | |||
| 671c32affe | |||
| bcadd7c47b | |||
| 59c5e350e1 | |||
| 6cd11536a0 | |||
| 4f01c0aa7a | |||
| 15667120b7 | |||
| 9ede8c70b9 | |||
| a680fa35d2 | |||
| 8ddd78c818 | |||
| ee262b9647 | |||
| 0379eb6c0f | |||
| bf299a0143 | |||
| ba4b166c96 | |||
| c058cf95e4 | |||
| bd3c24ed83 | |||
| eb38c36f80 | |||
| ec0a148ad4 | |||
| 260df3ae40 | |||
| dfa766e013 | |||
| 8c0c994385 | |||
| 694d67ada2 | |||
| d6755e4a72 | |||
| 9cacf5708b | |||
| ad5c66fad2 | |||
| d89e91143b | |||
| 96bcc4cff4 | |||
| a8eb7ae039 | |||
| 6eade2c1d0 | |||
| 148c1aca0a | |||
| 70517bd907 | |||
| d5f0d7d225 | |||
| b20a846071 | |||
| 6600d26ef1 | |||
| 3de34062db | |||
| 8e9d7b4fdb | |||
| 5757380a63 | |||
| c944205080 | |||
| d825174ccb | |||
| 6408b8a9d3 | |||
| 845b69a4ac | |||
| 0c77d84176 | |||
| 970c1c86ca | |||
| 902630fb14 | |||
| 4ca443038d | |||
| 47c56cbac9 |
@@ -0,0 +1,10 @@
|
||||
.git
|
||||
.github
|
||||
build
|
||||
ui
|
||||
docker
|
||||
docs
|
||||
k8s
|
||||
load-test
|
||||
metrics
|
||||
scripts
|
||||
@@ -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,4 +1,3 @@
|
||||
build
|
||||
|
||||
site/
|
||||
|
||||
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
# Mainflux Changelog
|
||||
|
||||
## Generation
|
||||
Mainflux release notes for the latest release can be obtained via:
|
||||
```
|
||||
make changelog
|
||||
```
|
||||
|
||||
Otherwise, whole log in a similar format can be observed via:
|
||||
```
|
||||
git log --pretty=oneline --abbrev-commit
|
||||
```
|
||||
|
||||
## 0.8.0 - 20. MAR 2019.
|
||||
### Features
|
||||
- MF-571 - Add Env.elm to set custom base URL (#654)
|
||||
- NOISSUE Added docs about docker-compose config overriding (#653)
|
||||
- MF-539 - Improve Bootstrap Service documentation (#646)
|
||||
- MF-596 - Add subtopic to RawMessage (#642)
|
||||
- NOISSUE - Prevent infinite loop in lora-adapter if Redis init fail (#647)
|
||||
- Corrected grammar and rephrased a few sentences to read nicely (#641)
|
||||
- MF-571 - Elm UI (#632)
|
||||
- MF-552 - Use event sourcing to keep Bootstrap service in sync with Things service (#603)
|
||||
- MF-540 - Add pagination in API responses for Bootstrap service (#575)
|
||||
- MF-600 - Handle custom LoRa Server application decoder (#608)
|
||||
- update docker-compose (#590)
|
||||
- Update generated code (#602)
|
||||
- Add generated files check (#601)
|
||||
- MF-597 - Removed legacy code as not needed anymore (#598)
|
||||
- NOISSUE - Added normalizer service to run script (#594)
|
||||
- Changed RawMessage (#587)
|
||||
- NOISSUE - fix CLI log (#581)
|
||||
- MF-519 - Refine Message (#567)
|
||||
- NOISSUE - Add name field for Bootstrap Config (#564)
|
||||
- Fix non-SenML message routing in normalizer (#573)
|
||||
- NOISSUE - Update authors list (#569)
|
||||
- Update lora.md (#568)
|
||||
- NOISSUE- Improve LoRa doc (#562)
|
||||
- MF-551 - Add metadata fields to Bootstrap Channels (#563)
|
||||
- Fix MQTT adapter by setting subscription queue (#561)
|
||||
- MF-558 - Add MQTT subtopics documentation (#559)
|
||||
- Fix regexp for SUB (#557)
|
||||
- Simplify MQTT topipc regexp (#555)
|
||||
- MF-429 -Enabled MQTT subtopic's (#554)
|
||||
- Add env var for number of concurrent messages (#545)
|
||||
- NOISSUE - Update doc and fix empty key bug (#544)
|
||||
- MF-370 - Simplify and refine CI (#541)
|
||||
- NOISSUE - Add connection commands to CLI (#542)
|
||||
- NOISSUE - Refine docs (#537)
|
||||
- Update licnese year (#533)
|
||||
- MF-513 - Add Bootstrapping service (#524)
|
||||
- Add dedicated env vars for event sourcing (#536)
|
||||
- NOISSUE - Fix docs (#535)
|
||||
- Add lora doc to getting-started.md (#529)
|
||||
- MF-483 - Enable channels and devices corresponding lists in backend (#520)
|
||||
- Add missing components doc to architecture.md (#531)
|
||||
|
||||
### Bugfixes
|
||||
- MF-639 Split Content-Type header field on semicolon and evaluate all substrings (#644)
|
||||
- MF-656 - Change bootstrap service port to 8200 (#658)
|
||||
- Replace crossOrigin with relative path and fix messaging bug (#645)
|
||||
- MF-579 Things & Channels returns 404 when not found or ID is malformed, not 500 (#633)
|
||||
- Fix run command in dev guide (#605)
|
||||
- MF-583 - Correct cmd/mongodb-reader HTTPServer log Info (#584)
|
||||
- Fix Dusan Maldenovic GitHub (#570)
|
||||
- Fix CLI docs (#566)
|
||||
- Fix pagination response for empty page (#547)
|
||||
- Fix swagger and provisioning docs (#546)
|
||||
- NOISSUE - Fix event sourcing client on LoRa adapter (#527)
|
||||
- Fix MQTT adapter scaling issue (#526)
|
||||
- NOISSUE - Fix subtopic regex and restrict empty subtopic parts (#659)
|
||||
- Fix missing css in container ui (#638)
|
||||
- NOISSUE - Fix lora-adapter Object decode (#610)
|
||||
- NOISSUE - Fix users logs in main.go (#577)
|
||||
- NOISSUE - Fix normalizer exposed port in docker-compose (#548)
|
||||
|
||||
### Summary
|
||||
https://github.com/mainflux/mainflux/milestone/9?closed=1
|
||||
|
||||
|
||||
## 0.7.0 - 08. DEC 2018.
|
||||
### Features
|
||||
|
||||
- MF-486 - Add provisioning command to CLI (#487)
|
||||
- Fix lora-adapter event store handlers (#492)
|
||||
- NOISSUE - Add LoRa route map validation and fix LoRa messages URL (#491)
|
||||
- MF-475 - Replace increment ID with UUID (#490)
|
||||
- MF-166 - Add lora-adapter service (#481)
|
||||
- NOISSUE - Add Makefile target to clean old imgs (#485)
|
||||
- MF-473 - Add metadata field to channel (#476)
|
||||
- Make CoAP ping period configurable (#469)
|
||||
- Add nginx ingress config to k8s services (#472)
|
||||
- Add CoAP section in getting-started (#468)
|
||||
- NOISSUE - Move CLI documentation from getting started guide to separate page (#470)
|
||||
- NOISSUE - Update Getting Started doc with CLI usage (#465)
|
||||
- Update CoAP docs with URL example (#463)
|
||||
- MF-447 - Add event sourcing to things service (#460)
|
||||
- Add TLS support to CoAP adapter and all readers (#459)
|
||||
- MF-417 - Implement SDK tests (#438)
|
||||
- MF-454 - Use message Time field as a time for InfluxDB points (#455)
|
||||
- NOISSUE - Add .dockerignore to project root (#457)
|
||||
- Update docker-compose so that every service has debug log level (#453)
|
||||
- NOISSUE - Add TLS flag for Mainflux services (#452)
|
||||
- MF-448 - Option for Postgres SSL Mode (#449)
|
||||
- MF-443 Update project dependencies (#444)
|
||||
- MF-426 - Add optional MF_CA_CERTS env variable to allow GRPC client to use TLS certs (#430)
|
||||
- Expose the InfluxDB and Cassandra ports to host (#441)
|
||||
- MF-374 - Bring back CoAP adapter (#413)
|
||||
|
||||
### Bugfixes
|
||||
- gRPC Load Balancing between http-adapter and things (#387)
|
||||
- MF-407 - Values of zero are being omitted (#434)
|
||||
|
||||
### Summary
|
||||
https://github.com/mainflux/mainflux/milestone/8?closed=1
|
||||
|
||||
|
||||
## 0.6.0 - 26. OCT 2018.
|
||||
### Features
|
||||
|
||||
- Added Go SDK (#357)
|
||||
- Updated NATS version (#412)
|
||||
- Added debbug level to MFX logger (#379)
|
||||
- Added Documentation for readers (#389)
|
||||
- Added Redis cache to improve performance (#382)
|
||||
|
||||
|
||||
## 0.5.1 - 05. SEP 2018.
|
||||
### Features
|
||||
- Improve performance by adding Redis cache (#382)
|
||||
|
||||
### Bugfixes
|
||||
- Mixed up name and type of the things (#375)
|
||||
- Fix MQTT topic (#380)
|
||||
|
||||
|
||||
## 0.5.0 - 28. AUG 2018
|
||||
### Features
|
||||
- InfluxDB Reader (#311)
|
||||
- Cassandra Reader (#313)
|
||||
- MongoDB Reader (#344)
|
||||
- MQTT Persistance via Redis (#328)
|
||||
- CLI integrated into monorepo (#216)
|
||||
- Normalizer logging (#333)
|
||||
- WS swagger doc (#337)
|
||||
- Payload renamed to Metadata (#343)
|
||||
- Protobuf files added (#363)
|
||||
- SPDX headers added (#325)
|
||||
|
||||
### Bugfixes
|
||||
- Docker network for InfluxDB (#346)
|
||||
- Vendor correct gRPC version (#340)
|
||||
|
||||
### Summary
|
||||
https://github.com/mainflux/mainflux/milestone/6?closed=1
|
||||
|
||||
|
||||
## 0.4.0 - 01. JUN 2018.
|
||||
* Integrated MQTT adapter (#165 )
|
||||
* Support for storing messages in MongoDB (#237)
|
||||
* Support for storing messages in InfluxDB (#236)
|
||||
* Use UUID PKs with auto-incremented values (#269 )
|
||||
* Replaced JWT with plain string tokens in things service (#268 )
|
||||
* Emit non-SenML messages (#239 )
|
||||
* Support for Grafana (#296)
|
||||
* Added WS Load test (#299 )
|
||||
|
||||
|
||||
## 0.3.0 - 14. MAY 2018.
|
||||
- CoAP API for message exchange (#186)
|
||||
- Split `manager` service into `clients` and `users` (#266)
|
||||
- Replaced ORM with raw SQL (#265)
|
||||
- Setup Kubernetes (#226, #273)
|
||||
- Fix docker compose (#274)
|
||||
- Integrated `dashflux` into monorepo (#258)
|
||||
- Integrated (*non-compatible*) `mqtt` into monorepo (#260)
|
||||
|
||||
|
||||
## 0.2.3 - 24. APR 2018.
|
||||
- Fix examples in the documentation (#243)
|
||||
- Add service name in info response (#241)
|
||||
- Improve code coverage in WS adapter (#242)
|
||||
|
||||
|
||||
## 0.2.2 - 23. APR 2018.
|
||||
- Setup load testing scenarios (#225)
|
||||
|
||||
|
||||
## 0.2.1 - 22. APR 2018.
|
||||
- Fixed `Content-Type` header checking (#238)
|
||||
|
||||
## 0.2.0 - 18. APR 2018
|
||||
- Protobuf message serialization (#192)
|
||||
- Websocket API for exchanging messages (#188)
|
||||
- Channel & client retrieval paging (#227)
|
||||
- Service instrumentation (#213)
|
||||
- `go-kit` based JSON logger (#212)
|
||||
- Project documentation (#218, #220)
|
||||
- API tests (#211, #224)
|
||||
|
||||
|
||||
## 0.1.2 - 18. MAR 2018.
|
||||
### Bug fixes
|
||||
|
||||
- Fixed go lint warnings (#189)
|
||||
- Compose failing startup (#185)
|
||||
- Added missing service startup messages (#190)
|
||||
@@ -1,7 +1,7 @@
|
||||
# Contributing to Mainflux
|
||||
|
||||
The following is a set of guidelines for contributing to Mainflux and its libraries, which are
|
||||
hosted in the [Mainflux Organization](https://github.com/mainflux) on GitHub.
|
||||
The following is a set of guidelines to contribute to Mainflux and its libraries, which are
|
||||
hosted on the [Mainflux Organization](https://github.com/mainflux) on GitHub.
|
||||
|
||||
This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0).
|
||||
By participating, you are expected to uphold this code. Please report unacceptable behavior to
|
||||
@@ -9,16 +9,15 @@ By participating, you are expected to uphold this code. Please report unacceptab
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Reporting issues is a great way to contribute to the project. We always appreciate a well-written,
|
||||
thorough bug reports.
|
||||
Reporting issues are a great way to contribute to the project. We are perpetually grateful about a well-written,
|
||||
thorough bug report.
|
||||
|
||||
Prior to raising a new issue, check out [our issue
|
||||
list](https://github.com/mainflux/mainflux/issues) to determine whether it already include the
|
||||
problem you are facing.
|
||||
Before raising a new issue, check [our issue
|
||||
list](https://github.com/mainflux/mainflux/issues) to determine if it already contains the
|
||||
problem that you are facing.
|
||||
|
||||
A good bug report shouldn't leave others needing to chase you up for more information. Please try to
|
||||
be as detailed as possible. The following questions might serve as a template for writing a detailed
|
||||
reports:
|
||||
A good bug report shouldn't leave others needing to chase you for more information. Please be as detailed as possible. The following questions might serve as a template for writing a detailed
|
||||
report:
|
||||
|
||||
- What were you trying to achieve?
|
||||
- What are the expected results?
|
||||
@@ -29,16 +28,16 @@ reports:
|
||||
## Pull requests
|
||||
|
||||
Good pull requests (e.g. patches, improvements, new features) are a fantastic help. They should
|
||||
remain focused in scope and avoid containing unrelated commits.
|
||||
remain focused in scope and avoid unrelated commits.
|
||||
|
||||
**Please ask first** before embarking any significant pull request (e.g. implementing new features,
|
||||
**Please ask first** before embarking on any significant pull request (e.g. implementing new features,
|
||||
refactoring code etc.), otherwise you risk spending a lot of time working on something that the
|
||||
maintainers might not want to merge into the project.
|
||||
|
||||
Please adhere to the coding conventions used throughout the project. If in doubt, consult the
|
||||
[Effective Go](https://golang.org/doc/effective_go.html) style guide.
|
||||
|
||||
To start contributing to the project, [fork](https://help.github.com/articles/fork-a-repo/) it,
|
||||
To contribute to the project, [fork](https://help.github.com/articles/fork-a-repo/) it,
|
||||
clone your fork repository, and configure the remotes:
|
||||
|
||||
```
|
||||
@@ -47,7 +46,7 @@ cd mainflux
|
||||
git remote add upstream https://github.com/mainflux/mainflux.git
|
||||
```
|
||||
|
||||
If you cloned a while ago, get the latest changes from upstream:
|
||||
If your cloned repository is behind the upstream commits, then get the latest changes from upstream:
|
||||
|
||||
```
|
||||
git checkout master
|
||||
@@ -63,8 +62,8 @@ git checkout -b MF-[issue-number]
|
||||
|
||||
Commit your changes in logical chunks. When you are ready to commit, make sure
|
||||
to write a Good Commit Message™. Consult the [Erlang's contributing guide](https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
|
||||
if you're not sure what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
|
||||
to group your commits into logical units of working before making them public.
|
||||
if you're unsure of what constitutes a Good Commit Message™. Use [interactive rebase](https://help.github.com/articles/about-git-rebase)
|
||||
to group your commits into logical units of work before making it public.
|
||||
|
||||
Note that every commit you make must be signed. By signing off your work you indicate that you
|
||||
are accepting the [Developer Certificate of Origin](https://developercertificate.org/).
|
||||
Generated
+330
-112
@@ -3,92 +3,151 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:6da51e5ec493ad2b44cb04129e2d0a068c8fb9bd6cb5739d199573558696bb94"
|
||||
name = "github.com/Azure/go-ansiterm"
|
||||
packages = [
|
||||
".",
|
||||
"winterm"
|
||||
"winterm",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d6e3b3328b783f23731bc4d058875b0371ff8109"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bf42be3cb1519bf8018dfd99720b1005ee028d947124cab3ccf965da59381df6"
|
||||
name = "github.com/Microsoft/go-winio"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "7da180ee92d8bd8bb8c37fc560e673e6557c392f"
|
||||
version = "v0.4.7"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:3721a10686511b80c052323423f0de17a8c06d417dbdd3b392b1578432a33aae"
|
||||
name = "github.com/Nvveen/Gotty"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "cd527374f1e5bff4938207604a14f2e38a9cf512"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:320e7ead93de9fd2b0e59b50fd92a4d50c1f8ab455d96bc2eb083267453a9709"
|
||||
name = "github.com/asaskevich/govalidator"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ccb8e960c48f04d6935e72476ae4a51028f9e22f"
|
||||
version = "v9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
pruneopts = "UT"
|
||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/buger/jsonparser"
|
||||
packages = ["."]
|
||||
revision = "2cac668e8456b4284edb0715e17e2af02d3ec993"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2209584c0f7c9b68c23374e659357ab546e1b70eec2761f03280f69a8fd23d77"
|
||||
name = "github.com/cenkalti/backoff"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "2ea60e5f094469f9e65adb9cd103795b73ae743e"
|
||||
version = "v2.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:bb300c7f4b52deb1fcc65f6be624ae3dc289453824c2f1f184963dc41d941a68"
|
||||
name = "github.com/cisco/senml"
|
||||
packages = ["."]
|
||||
revision = "448c9510575e1dd6f780cb10addbad998cf80418"
|
||||
pruneopts = "UT"
|
||||
revision = "910a55054e168c1122b905e3f8acdc5ff97cbec1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:fc8dbcc2a5de7c093e167828ebbdf551641761d2ad75431d3a167d467a264115"
|
||||
name = "github.com/containerd/continuity"
|
||||
packages = ["pathdriver"]
|
||||
pruneopts = "UT"
|
||||
revision = "c6cef34830231743494fe2969284df7b82cc0ad0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
pruneopts = "UT"
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:ca71cdedcb487a361fe2588afeb3d373c0dfe21feaac229270fd6de234bd3c5e"
|
||||
name = "github.com/dereulenspiegel/coap-mux"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "e137e310a99955be01d60b2237da89153ea40987"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55"
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a5036bc703c3994f70ced9b34e9941381aa6bd03e69a0ce538f43bf14dc8dc98"
|
||||
name = "github.com/docker/docker"
|
||||
packages = [
|
||||
"pkg/namesgenerator",
|
||||
"pkg/random",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "092cba3727bb9b4a2f0e922cd6c0f93ea270e363"
|
||||
version = "v1.13.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:87dcb59127512b84097086504c16595cf8fef35b9e0bfca565dfc06e198158d7"
|
||||
name = "github.com/docker/go-connections"
|
||||
packages = ["nat"]
|
||||
pruneopts = "UT"
|
||||
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6f82cacd0af5921e99bf3f46748705239b36489464f4529a1589bc895764fb18"
|
||||
name = "github.com/docker/go-units"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "47565b4f722fb6ceae66b95f853feed578a4a51c"
|
||||
version = "v0.3.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f77ee3cd73cbf60434b4f72ec46a66773c555d46764399735b5f9a28505f3ece"
|
||||
name = "github.com/dustin/go-coap"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:184008c955d6a3226b700c13ed0e875a7a051a759618f9bccba9b5c99f17faa5"
|
||||
name = "github.com/eclipse/paho.mqtt.golang"
|
||||
packages = [
|
||||
".",
|
||||
"packets",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "36d01c2b4cbeb3d2a12063e4880ce30800af9560"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:865079840386857c809b72ce300be7580cb50d3d3129ce11bf9aa6ca2bc1934a"
|
||||
name = "github.com/fatih/color"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
|
||||
version = "v1.7.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:90aaa664c659918dd0055e29eb5fd6b96a17a779d423db3dd3f5764c5dbbbd4f"
|
||||
name = "github.com/go-kit/kit"
|
||||
packages = [
|
||||
"endpoint",
|
||||
@@ -97,226 +156,273 @@
|
||||
"metrics/internal/lv",
|
||||
"metrics/prometheus",
|
||||
"transport/grpc",
|
||||
"transport/http"
|
||||
"transport/http",
|
||||
]
|
||||
revision = "4dc7be5d2d12881735283bcab7352178e190fc71"
|
||||
version = "v0.6.0"
|
||||
pruneopts = "UT"
|
||||
revision = "ca4112baa34cb55091301bdc13b1420a122b1b9e"
|
||||
version = "v0.7.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:31a18dae27a29aa074515e43a443abfd2ba6deb6d69309d8d7ce789c45f34659"
|
||||
name = "github.com/go-logfmt/logfmt"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:424f6593024cdf0f6f90cba81bc69ca98df3758525e6fb248198ef15ead603a9"
|
||||
name = "github.com/go-redis/redis"
|
||||
packages = [
|
||||
".",
|
||||
"internal",
|
||||
"internal/consistenthash",
|
||||
"internal/hashtag",
|
||||
"internal/pool",
|
||||
"internal/proto",
|
||||
"internal/singleflight",
|
||||
"internal/util",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "7f89fbac80bcc62ce920b6dbc6ca60238d7725d1"
|
||||
version = "v6.15.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c4a2528ccbcabf90f9f3c464a5fc9e302d592861bbfd0b7135a7de8a943d0406"
|
||||
name = "github.com/go-stack/stack"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
|
||||
version = "v1.7.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8dfa0b1b7c4b7398d46340aef0f2a58aad5a75959eeeb79e023ab826f0c7f7f0"
|
||||
name = "github.com/go-zoo/bone"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "fd0aebc74e908868b09ac140fb5a53cb363884c1"
|
||||
version = "1.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:348bbd07b4a983b2f811f81948c78142de09bb9fcf25284bee5e2ab3bdd0f6f9"
|
||||
name = "github.com/gocql/gocql"
|
||||
packages = [
|
||||
".",
|
||||
"internal/lru",
|
||||
"internal/murmur",
|
||||
"internal/streams"
|
||||
"internal/streams",
|
||||
]
|
||||
revision = "651d6b1f343c6e168e45d119dcfe8f71e5bafb32"
|
||||
pruneopts = "UT"
|
||||
revision = "68ae1e384be4d7cd7df0f5b77f331759d806308e"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bbadccf3d3317ea03c0dac0b45b673b4b397c8f91a1d2eff550a3c51c4ad770e"
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
|
||||
version = "v1.0.0"
|
||||
pruneopts = "UT"
|
||||
revision = "636bf0302bc95575d69441b25a2603156ffdddf1"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4c0989ca0bcd10799064318923b9bc2db6b4d6338dd75f3f2d86c3511aaaf5cf"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = [
|
||||
"proto",
|
||||
"ptypes",
|
||||
"ptypes/any",
|
||||
"ptypes/duration",
|
||||
"ptypes/timestamp"
|
||||
"ptypes/timestamp",
|
||||
]
|
||||
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"
|
||||
pruneopts = "UT"
|
||||
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4a0c6bb4805508a6287675fac876be2ac1182539ca8a32468d8128882e9d5009"
|
||||
name = "github.com/golang/snappy"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d"
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:364b908b9b27b97ab838f2f6f1b1f46281fa29b978a037d72a9b1d4f6d940190"
|
||||
name = "github.com/hailocab/go-hostpool"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "e80d13ce29ede4452c43dea11e79b9bc8a15b478"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:6727359c2ab1efe714cc87acb36be6cf15a3aae078d8ae736a17fec787dab307"
|
||||
name = "github.com/hokaccha/go-prettyjson"
|
||||
packages = ["."]
|
||||
revision = "e6b9231a2b1cc5798b336fdbf99c16cd8ad1e0c9"
|
||||
pruneopts = "UT"
|
||||
revision = "f579f869bbfea48a61fbea09207cadd1aaeceb83"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:65b0a07f85f7b5cc019c26775efc278a155a5dea0a8aa617c980e8308d16bc55"
|
||||
name = "github.com/influxdata/influxdb"
|
||||
packages = [
|
||||
"client/v2",
|
||||
"models",
|
||||
"pkg/escape"
|
||||
"pkg/escape",
|
||||
]
|
||||
revision = "02d7d4f043b34ecb4e9b2dbec298c6f9450c2a32"
|
||||
version = "v1.5.2"
|
||||
pruneopts = "UT"
|
||||
revision = "a2ba6e7654fb078f8a3f5add1f8d935df38161bd"
|
||||
version = "v1.6.4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a64e323dc06b73892e5bb5d040ced475c4645d456038333883f58934abbf6f72"
|
||||
name = "github.com/kr/logfmt"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:1e537dfb7b4d543e25efa2c812084dd65736f8010979c1b7e167a5b876563199"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
"oid"
|
||||
"oid",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d4d17353dbd05cb52a2a52b7fe1771883b682806f68db442b436294926bbfafb"
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6"
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
packages = ["pbutil"]
|
||||
pruneopts = "UT"
|
||||
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:27fa1b385644151ee1fc0c8b04f1f86963d9701335491b94488fd3912e370354"
|
||||
name = "github.com/mongodb/mongo-go-driver"
|
||||
packages = [
|
||||
"bson",
|
||||
"bson/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",
|
||||
"bson/bsoncodec",
|
||||
"bson/bsonrw",
|
||||
"bson/bsontype",
|
||||
"bson/primitive",
|
||||
"event",
|
||||
"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"
|
||||
"mongo/options",
|
||||
"mongo/readconcern",
|
||||
"mongo/readpref",
|
||||
"mongo/writeconcern",
|
||||
"tag",
|
||||
"version",
|
||||
"x/bsonx",
|
||||
"x/bsonx/bsoncore",
|
||||
"x/mongo/driver",
|
||||
"x/mongo/driver/auth",
|
||||
"x/mongo/driver/auth/internal/gssapi",
|
||||
"x/mongo/driver/session",
|
||||
"x/mongo/driver/topology",
|
||||
"x/mongo/driver/uuid",
|
||||
"x/network/address",
|
||||
"x/network/command",
|
||||
"x/network/compressor",
|
||||
"x/network/connection",
|
||||
"x/network/connstring",
|
||||
"x/network/description",
|
||||
"x/network/result",
|
||||
"x/network/wiremessage",
|
||||
]
|
||||
revision = "8df56f107cdd485351eb2523992c3364114b4b2e"
|
||||
version = "v0.0.10"
|
||||
pruneopts = "UT"
|
||||
revision = "3cfdc9ffcf780eede57d27ec26a3edad4c9ab6a9"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4ce94f3d2074662c04677db471456feb56078244ebcfe3789e6e95901c085bc5"
|
||||
name = "github.com/nats-io/go-nats"
|
||||
packages = [
|
||||
".",
|
||||
"encoders/builtin",
|
||||
"util"
|
||||
"util",
|
||||
]
|
||||
revision = "062418ea1c2181f52dc0f954f6204370519a868b"
|
||||
version = "v1.5.0"
|
||||
pruneopts = "UT"
|
||||
revision = "fb0396ee0bdb8018b0fef30d6d1de798ce99cd05"
|
||||
version = "v1.6.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c3cd663f2f30b92536b9f290ac85c6310dae36a14cb8961553ae9ccf0d85ae41"
|
||||
name = "github.com/nats-io/nuid"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "289cccf02c178dc782430d534e3c1f5b72af807f"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ee4d4af67d93cc7644157882329023ce9a7bcfce956a079069a9405521c7cc8d"
|
||||
name = "github.com/opencontainers/go-digest"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
|
||||
version = "v1.0.0-rc1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:11db38d694c130c800d0aefb502fb02519e514dc53d9804ce51d1ad25ec27db6"
|
||||
name = "github.com/opencontainers/image-spec"
|
||||
packages = [
|
||||
"specs-go",
|
||||
"specs-go/v1"
|
||||
"specs-go/v1",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d60099175f88c47cd379c4738d158884749ed235"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1869683e323ebff2bdf8adcb560f82bf6f8d94019d35099e3403f7df12e9c07e"
|
||||
name = "github.com/opencontainers/runc"
|
||||
packages = [
|
||||
"libcontainer/system",
|
||||
"libcontainer/user"
|
||||
"libcontainer/user",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3b830e06971be8448e7c9e57c9cc15ce53ab7e8846e7b7c39110a177c7fae466"
|
||||
name = "github.com/ory/dockertest"
|
||||
packages = [
|
||||
"docker",
|
||||
@@ -342,126 +448,175 @@
|
||||
"docker/types/network",
|
||||
"docker/types/registry",
|
||||
"docker/types/strslice",
|
||||
"docker/types/versions"
|
||||
"docker/types/versions",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "2e92e7784b6fb199fd168aa46269a2f1b34f299e"
|
||||
version = "v3.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = "UT"
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:26663fafdea73a38075b07e8e9d82fc0056379d2be8bb4e13899e8fda7c7dd23"
|
||||
name = "github.com/prometheus/client_golang"
|
||||
packages = [
|
||||
"prometheus",
|
||||
"prometheus/promhttp"
|
||||
"prometheus/internal",
|
||||
"prometheus/promhttp",
|
||||
]
|
||||
revision = "c5b7fccd204277076155f10851dad72b76a49317"
|
||||
version = "v0.8.0"
|
||||
pruneopts = "UT"
|
||||
revision = "abad2d1bd44235a26707c172eab6bca5bf2dbad3"
|
||||
version = "v0.9.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b"
|
||||
name = "github.com/prometheus/client_model"
|
||||
packages = ["go"]
|
||||
pruneopts = "UT"
|
||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e469cd65badf7694aeb44874518606d93c1d59e7735d3754ad442782437d3cc3"
|
||||
name = "github.com/prometheus/common"
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model"
|
||||
"model",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:570784e0ddbf67f14087c6d8cfb7b999b9c55e75518a755e88cb202566ea8a17"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/util",
|
||||
"nfs",
|
||||
"xfs"
|
||||
"xfs",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:89cf776c0a621a34017c819b66b9e0f0bb076d3c5b9ca74763fb89570e140038"
|
||||
name = "github.com/rubenv/sql-migrate"
|
||||
packages = [
|
||||
".",
|
||||
"sqlparse"
|
||||
"sqlparse",
|
||||
]
|
||||
revision = "081fe17d19ff4e2dd9f5a0c1158e6bcf74da6906"
|
||||
pruneopts = "UT"
|
||||
revision = "ba2c6a7295c59448dbc195cef2f41df5163b3892"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:274f67cb6fed9588ea2521ecdac05a6d62a8c51c074c1fccc6a49a40ba80e925"
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9e9193aa51197513b3abcb108970d831fbcf40ef96aa845c4f03276e1fa316d2"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d3ffbc45dfe6929a4ba313464c2d761d96325d306eda29512da56c034db2a4dd"
|
||||
name = "github.com/sony/gobreaker"
|
||||
packages = ["."]
|
||||
revision = "e9556a45379ef1da12e54847edb2fb3d7d566f36"
|
||||
version = "0.3.0"
|
||||
pruneopts = "UT"
|
||||
revision = "b2a34562d02c4d5fd6645399c69e00141fb24e5a"
|
||||
version = "0.4.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:645cabccbb4fa8aab25a956cbcbdf6a6845ca736b2c64e197ca7cbb9d210b939"
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9424f440bba8f7508b69414634aef3b2b3a877e522d8a4624692412805407bb7"
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "583c0c0531f06d5278b7d917446061adc344b5cd"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c40d65817cdd41fac9aa7af8bed56927bb2d6d47e4fea566a74880f5c2b1c41e"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = [
|
||||
"assert",
|
||||
"require"
|
||||
"require",
|
||||
]
|
||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||
version = "v1.2.1"
|
||||
pruneopts = "UT"
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:03aa6e485e528acb119fb32901cf99582c380225fc7d5a02758e08b180cb56c3"
|
||||
name = "github.com/ugorji/go"
|
||||
packages = ["codec"]
|
||||
pruneopts = "UT"
|
||||
revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:40fdfd6ab85ca32b6935853bbba35935dcb1d796c8135efd85947566c76e662e"
|
||||
name = "github.com/xdg/scram"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "7eeb5667e42c09cb51bf7b7c28aea8c56767da90"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f5c1d04bc09c644c592b45b9f0bad4030521b1a7d11c7dadbb272d9439fa6e8e"
|
||||
name = "github.com/xdg/stringprep"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "73f8eece6fdcd902c185bf651de50f3828bed5ed"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:7310f5459b88177d24e26c5ec7deb100b78ec03c0437d7a7aaec3c7eac333a55"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"bcrypt",
|
||||
"blowfish",
|
||||
"pbkdf2",
|
||||
"ssh/terminal"
|
||||
"ssh/terminal",
|
||||
]
|
||||
revision = "2d027ae1dddd4694d54f7a8b6cbe78dca8720226"
|
||||
pruneopts = "UT"
|
||||
revision = "4d3f4d9ffa16a13f451c3b2999e9c49e9750bf06"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:70e155783b8dfd4f7697243384e2f361930ff0751459580769ce44d511f38b05"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
@@ -469,27 +624,36 @@
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
"idna",
|
||||
"internal/socks",
|
||||
"internal/timeseries",
|
||||
"trace"
|
||||
"proxy",
|
||||
"trace",
|
||||
"websocket",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "f73e4c9ed3b7ebdd5f699a16a880c2b1994e50dd"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e0140c0c868c6e0f01c0380865194592c011fe521d6e12d78bfd33e756fe018a"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["semaphore"]
|
||||
pruneopts = "UT"
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:66e34aa65b83061616603ba83517a4523c7261e18afd01c769dd215614bf7d70"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows"
|
||||
"windows",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "7dfd1290c7917b7ba22824b9d24954ab3002fe24"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
@@ -505,33 +669,40 @@
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable"
|
||||
"unicode/rangetable",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:cd018653a358d4b743a9d3bee89e825521f2ab2f2ec0770164bf7632d8d73ab7"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
pruneopts = "UT"
|
||||
revision = "86e600f69ee4704c6efbf6a2a40a5c10700e76c2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c3ad9841823db6da420a5625b367913b4ff54bbe60e8e3c98bd20e243e62e2d2"
|
||||
name = "google.golang.org/grpc"
|
||||
packages = [
|
||||
".",
|
||||
"balancer",
|
||||
"balancer/base",
|
||||
"balancer/roundrobin",
|
||||
"channelz",
|
||||
"codes",
|
||||
"connectivity",
|
||||
"credentials",
|
||||
"encoding",
|
||||
"encoding/proto",
|
||||
"grpclb/grpc_lb_v1/messages",
|
||||
"grpclog",
|
||||
"internal",
|
||||
"internal/backoff",
|
||||
"internal/channelz",
|
||||
"internal/envconfig",
|
||||
"internal/grpcrand",
|
||||
"internal/transport",
|
||||
"keepalive",
|
||||
"metadata",
|
||||
"naming",
|
||||
@@ -542,32 +713,79 @@
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
"transport"
|
||||
]
|
||||
revision = "41344da2231b913fa3d983840a57a6b1b7b631a1"
|
||||
version = "v1.12.0"
|
||||
pruneopts = "UT"
|
||||
revision = "2e463a05d100327ca47ac218281906921038fd95"
|
||||
version = "v1.16.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fa9a7c0ef59217bd22f32eb7cc027894d73f340a5633258cc079dec025db6a7f"
|
||||
name = "gopkg.in/gorp.v1"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "c87af80f3cc5036b55b83d77171e156791085e2e"
|
||||
version = "v1.7.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a"
|
||||
name = "gopkg.in/inf.v0"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf"
|
||||
version = "v0.9.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:de4bc452b049bde272d99e944aabcf218dc22df0a478a42a7107932f82430ae2"
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
packages = ["."]
|
||||
revision = "2e92e7784b6fb199fd168aa46269a2f1b34f299e"
|
||||
version = "v3.3.0"
|
||||
pruneopts = "UT"
|
||||
revision = "9f1141b78507d23603f6be530587664ebb63a8d2"
|
||||
version = "v3.3.2"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "e3b07d2098253644fc2f77fede1f00fcc077a20e4f46c59466ffd61c4998bb00"
|
||||
input-imports = [
|
||||
"github.com/asaskevich/govalidator",
|
||||
"github.com/cisco/senml",
|
||||
"github.com/dereulenspiegel/coap-mux",
|
||||
"github.com/dgrijalva/jwt-go",
|
||||
"github.com/docker/docker/pkg/namesgenerator",
|
||||
"github.com/dustin/go-coap",
|
||||
"github.com/eclipse/paho.mqtt.golang",
|
||||
"github.com/fatih/color",
|
||||
"github.com/go-kit/kit/endpoint",
|
||||
"github.com/go-kit/kit/log",
|
||||
"github.com/go-kit/kit/metrics",
|
||||
"github.com/go-kit/kit/metrics/prometheus",
|
||||
"github.com/go-kit/kit/transport/grpc",
|
||||
"github.com/go-kit/kit/transport/http",
|
||||
"github.com/go-redis/redis",
|
||||
"github.com/go-zoo/bone",
|
||||
"github.com/gocql/gocql",
|
||||
"github.com/gogo/protobuf/proto",
|
||||
"github.com/golang/protobuf/proto",
|
||||
"github.com/gorilla/websocket",
|
||||
"github.com/hokaccha/go-prettyjson",
|
||||
"github.com/influxdata/influxdb/client/v2",
|
||||
"github.com/lib/pq",
|
||||
"github.com/mongodb/mongo-go-driver/mongo",
|
||||
"github.com/nats-io/go-nats",
|
||||
"github.com/prometheus/client_golang/prometheus",
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
||||
"github.com/rubenv/sql-migrate",
|
||||
"github.com/satori/go.uuid",
|
||||
"github.com/sony/gobreaker",
|
||||
"github.com/spf13/cobra",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/stretchr/testify/require",
|
||||
"golang.org/x/crypto/bcrypt",
|
||||
"golang.org/x/net/context",
|
||||
"google.golang.org/grpc",
|
||||
"google.golang.org/grpc/codes",
|
||||
"google.golang.org/grpc/credentials",
|
||||
"google.golang.org/grpc/status",
|
||||
"gopkg.in/ory-am/dockertest.v3",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+39
-15
@@ -3,20 +3,24 @@
|
||||
version = "9.0.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/cisco/senml"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
version = "3.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/eclipse/paho.mqtt.golang"
|
||||
version = "1.1.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fatih/color"
|
||||
version = "1.7.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-kit/kit"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-zoo/bone"
|
||||
@@ -24,23 +28,23 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/hokaccha/go-prettyjson"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/nats-io/go-nats"
|
||||
version = "1.4.0"
|
||||
version = "1.6.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
version = "0.8.0"
|
||||
version = "0.9.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
@@ -48,7 +52,7 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sony/gobreaker"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/cobra"
|
||||
@@ -56,35 +60,55 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
version = "3.1.6"
|
||||
version = "3.3.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
version = "1.2.0"
|
||||
version = "1.4.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/rubenv/sql-migrate"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/influxdata/influxdb"
|
||||
version = "1.5.2"
|
||||
version = "1.6.4"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mongodb/mongo-go-driver"
|
||||
version = "0.0.5"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/gocql/gocql"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/dereulenspiegel/coap-mux"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/dustin/go-coap"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
version = "1.1.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "google.golang.org/grpc"
|
||||
version = "1.16.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-redis/redis"
|
||||
version = "v6.15.0"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015-2018 Mainflux
|
||||
Copyright 2015-2019 Mainflux
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
+14
-13
@@ -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"
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
## Copyright (c) 2015-2019
|
||||
## Mainflux
|
||||
##
|
||||
## SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
BUILD_DIR = build
|
||||
SERVICES = users things http normalizer ws influxdb-writer influxdb-reader mongodb-writer mongodb-reader cassandra-writer cassandra-reader cli
|
||||
SERVICES = users things http normalizer ws coap lora influxdb-writer influxdb-reader mongodb-writer mongodb-reader cassandra-writer cassandra-reader cli bootstrap
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOOS ?= linux
|
||||
|
||||
@@ -9,25 +15,46 @@ 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
|
||||
|
||||
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
|
||||
|
||||
all: $(SERVICES) mqtt
|
||||
|
||||
.PHONY: all $(SERVICES) dockers latest release mqtt
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release mqtt ui
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
rm -rf mqtt/node_modules
|
||||
|
||||
cleandocker: cleanghost
|
||||
# Stop all containers (if running)
|
||||
docker-compose -f docker/docker-compose.yml stop
|
||||
# Remove mainflux containers
|
||||
docker ps -f name=mainflux -aq | xargs -r docker rm
|
||||
# Remove old mainflux images
|
||||
docker images -q mainflux\/* | xargs -r docker rmi
|
||||
|
||||
# Clean ghost docker images
|
||||
cleanghost:
|
||||
# Remove exited containers
|
||||
docker ps -f status=dead -f status=exited -aq | xargs -r docker rm -v
|
||||
# Remove unused images
|
||||
docker images -f dangling=true -q | xargs -r docker rmi
|
||||
# Remove unused volumes
|
||||
docker volume ls -f dangling=true -q | xargs -r docker volume rm
|
||||
|
||||
install:
|
||||
cp ${BUILD_DIR}/* $(GOBIN)
|
||||
|
||||
test:
|
||||
GOCACHE=off go test -v -race -tags test $(shell go list ./... | grep -v 'vendor\|cmd')
|
||||
go test -v -race -count 1 -tags test $(shell go list ./... | grep -v 'vendor\|cmd')
|
||||
|
||||
proto:
|
||||
protoc --go_out=plugins=grpc:. *.proto
|
||||
protoc --gofast_out=plugins=grpc:. *.proto
|
||||
|
||||
$(SERVICES):
|
||||
$(call compile_service,$(@))
|
||||
@@ -35,19 +62,39 @@ $(SERVICES):
|
||||
$(DOCKERS):
|
||||
$(call make_docker,$(@))
|
||||
|
||||
dockers: $(DOCKERS)
|
||||
docker build --tag=mainflux/dashflux -f dashflux/docker/Dockerfile dashflux
|
||||
docker_ui:
|
||||
$(MAKE) -C ui docker
|
||||
|
||||
docker_mqtt:
|
||||
# MQTT Docker build must be done from root dir because it copies .proto files
|
||||
docker build --tag=mainflux/mqtt -f mqtt/Dockerfile .
|
||||
|
||||
dockers: $(DOCKERS) docker_ui docker_mqtt
|
||||
|
||||
$(DOCKERS_DEV):
|
||||
$(call make_docker_dev,$(@))
|
||||
|
||||
dockers_dev: $(DOCKERS_DEV)
|
||||
|
||||
ui:
|
||||
$(MAKE) -C ui
|
||||
|
||||
mqtt:
|
||||
cd mqtt && npm install
|
||||
|
||||
latest: dockers
|
||||
define docker_push
|
||||
for svc in $(SERVICES); do \
|
||||
docker push mainflux/$$svc; \
|
||||
docker push mainflux/$$svc:$(1); \
|
||||
done
|
||||
docker push mainflux/dashflux
|
||||
docker push mainflux/mqtt
|
||||
docker push mainflux/ui:$(1)
|
||||
docker push mainflux/mqtt:$(1)
|
||||
endef
|
||||
|
||||
changelog:
|
||||
git log $(shell git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s"
|
||||
|
||||
latest: dockers
|
||||
$(call docker_push,latest)
|
||||
|
||||
release:
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
@@ -55,12 +102,21 @@ release:
|
||||
$(MAKE) dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
|
||||
docker push mainflux/$$svc:$(version); \
|
||||
done
|
||||
docker tag mainflux/dashflux mainflux/dashflux:$(version)
|
||||
docker push mainflux/dashflux:$(version)
|
||||
docker tag mainflux/ui mainflux/ui:$(version)
|
||||
docker tag mainflux/mqtt mainflux/mqtt:$(version)
|
||||
docker push mainflux/mqtt:$(version)
|
||||
$(call docker_push,$(version))
|
||||
|
||||
rundev:
|
||||
cd scripts && ./run.sh
|
||||
|
||||
run:
|
||||
cd scripts && ./run.sh
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
|
||||
runui:
|
||||
$(MAKE) -C ui run
|
||||
|
||||
runlora:
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
docker-compose -f docker/addons/influxdb-writer/docker-compose.yml up -d
|
||||
docker-compose -f docker/addons/lora-adapter/docker-compose.yml up
|
||||
|
||||
@@ -16,16 +16,25 @@ for building complex IoT solutions.
|
||||
|
||||
For more details, check out the [official documentation][docs].
|
||||
|
||||
## Features
|
||||
Mainflux is member of the [Linux Foundation][lf] and an active contributor
|
||||
to the [EdgeX Foundry][edgex] project. It has been made with :heart: by [Mainflux company][company],
|
||||
which maintains the project and offers professional services around it.
|
||||
|
||||
- Protocol bridging (i.e. HTTP, MQTT, WebSocket, CoAP)
|
||||
## Features
|
||||
- Multi-protocol connectivity and protocol bridging (HTTP, MQTT, WebSocket and CoAP)
|
||||
- Device management and provisioning
|
||||
- Fine-grained access control
|
||||
- Storage support (Cassandra, InfluxDB and MongoDB)
|
||||
- Platform logging and instrumentation support
|
||||
- Container-based deployment using [Docker][docker]
|
||||
|
||||
## Quickstart
|
||||
- Event sourcing
|
||||
- Container-based deployment using [Docker][docker] and [Kubernetes][kubernetes]
|
||||
- [LoRaWAN][lora] network integration
|
||||
- SDK
|
||||
- CLI
|
||||
- Small memory footprint and fast execution
|
||||
- Domain-driven design architecture, high-quality code and test coverage
|
||||
|
||||
## Install
|
||||
Before proceeding, install the following prerequisites:
|
||||
|
||||
- [Docker](https://docs.docker.com/install/)
|
||||
@@ -37,20 +46,65 @@ Once everything is installed, execute the following commands from project root:
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## Contributing
|
||||
This will bring up all Mainflux dockers and inter-connect them in the composition.
|
||||
|
||||
## Usage
|
||||
Best way to quickstart using Mainflux is via CLI:
|
||||
```
|
||||
make cli
|
||||
./build/mainflux-cli version
|
||||
```
|
||||
|
||||
> Mainflux CLI can also be downloaded as a tarball from [offical release page][rel]
|
||||
|
||||
If this works, head to [official documentation][docs] to understand Mainflux provisioning and messaging.
|
||||
|
||||
## Documentation
|
||||
Official documentation is hosted at [Mainflux Read The Docs page][docs].
|
||||
|
||||
Documentation is auto-generated from Markdown files in `./docs` directory.
|
||||
If you spot an error or need for corrections, please let us know - or even better: send us a PR.
|
||||
|
||||
Additional practical information, news and tutorials can be found on the [Mainflux blog][blog].
|
||||
|
||||
## Authors
|
||||
Main architect and BDFL of Mainflux project is [@drasko][drasko].
|
||||
|
||||
Additionally, [@nmarcetic][nikola] and [@janko-isidorovic][janko] assured
|
||||
overall architecture and design, while [@manuio][manu] and [@darkodraskovic][darko]
|
||||
helped with crafting initial implementation and continiusly work on the project evolutions.
|
||||
|
||||
Besides them, Mainflux is constantly improved and actively
|
||||
developed by [@anovakovic01][alex], [@dusanb94][dusan], [@srados][sava],
|
||||
[@gsaleh][george], [@blokovi][iva], [@chombium][kole] and a large set of contributors.
|
||||
|
||||
Maintainers are listed in [MAINTAINERS](MAINTAINERS) file.
|
||||
|
||||
Mainflux team would like to give special thanks to [@mijicd][dejan] for his monumental work
|
||||
on designing and implementing highly improved and optimized version of the platform,
|
||||
and [@malidukica][dusanm] for his effort on implementing initial user interface.
|
||||
|
||||
## Contributing
|
||||
Thank you for your interest in Mainflux and wish to contribute!
|
||||
|
||||
1. Take a look at our [open issues](https://github.com/mainflux/mainflux/issues).
|
||||
2. Checkout the [contribution guide](.github/CONTRIBUTING.md) to learn more about our style and conventions.
|
||||
2. Checkout the [contribution guide](CONTRIBUTING.md) to learn more about our style and conventions.
|
||||
3. Make your changes compatible to our workflow.
|
||||
|
||||
## Community
|
||||
### We're Hiring
|
||||
If you are interested in working professionally on Mainflux,
|
||||
please head to company's [careers page][careers] or shoot us an e-mail at <careers@mainflux.com>.
|
||||
|
||||
Note that the best way to grab our attention is by sending PRs :sunglasses:.
|
||||
|
||||
## Community
|
||||
- [Google group][forum]
|
||||
- [Gitter][gitter]
|
||||
- [Twitter][twitter]
|
||||
|
||||
## License
|
||||
[Apache-2.0](LICENSE)
|
||||
|
||||
[banner]: https://github.com/mainflux/mainflux/blob/master/docs/img/gopherBanner.jpg
|
||||
[ci-badge]: https://semaphoreci.com/api/v1/mainflux/mainflux/branches/master/badge.svg
|
||||
[ci-url]: https://semaphoreci.com/mainflux/mainflux
|
||||
@@ -65,3 +119,24 @@ Thank you for your interest in Mainflux and wish to contribute!
|
||||
[cov-url]: https://codecov.io/gh/mainflux/mainflux
|
||||
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
|
||||
[twitter]: https://twitter.com/mainflux
|
||||
[lora]: https://lora-alliance.org/
|
||||
[kubernetes]: https://kubernetes.io/
|
||||
[rel]: https://github.com/mainflux/mainflux/releases
|
||||
[careers]: https://www.mainflux.com/careers.html
|
||||
[lf]: https://www.linuxfoundation.org/
|
||||
[edgex]: https://www.edgexfoundry.org/
|
||||
[company]: https://www.mainflux.com/
|
||||
[blog]: https://medium.com/mainflux-iot-platform
|
||||
[drasko]: https://github.com/drasko
|
||||
[nikola]: https://github.com/nmarcetic
|
||||
[dejan]: https://github.com/mijicd
|
||||
[manu]: https://github.com/manuIO
|
||||
[darko]: https://github.com/darkodraskovic
|
||||
[janko]: https://github.com/janko-isidorovic
|
||||
[alex]: https://github.com/anovakovic01
|
||||
[dusan]: https://github.com/dusanb94
|
||||
[sava]: https://github.com/srados
|
||||
[george]: https://github.com/gesaleh
|
||||
[iva]: https://github.com/blokovi
|
||||
[kole]: https://github.com/chombium
|
||||
[dusanm]: https://github.com/malidukica
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# BOOTSTRAP SERVICE
|
||||
|
||||
New devices need to be configured properly and connected to the Mainflux. Bootstrap service is used in order to accomplish that. This service provides the following features:
|
||||
1) Creating new Mainflux Things
|
||||
2) Providing basic configuration for the newly created Things
|
||||
3) Enabling/disabling Things
|
||||
|
||||
Pre-provisioning a new Thing is as simple as sending Configuration data to the Bootstrap service. Once the Thing is online, it sends a request for initial config to Bootstrap service. Bootstrap service provides an API for enabling and disabling Things. Only enabled Things can exchange messages over Mainflux. Bootstrapping does not implicitly enable Things, it has to be done manually.
|
||||
|
||||
In order to bootstrap successfully, the Thing needs to send bootstrapping request to the specific URL, as well as a secret key. This key and URL are pre-provisioned during the manufacturing process. If the Thing is provisioned on the Bootstrap service side, the corresponding configuration will be sent as a response. Otherwise, the Thing will be saved so that it can be provisioned later.
|
||||
|
||||
***Thing Configuration***
|
||||
|
||||
Thing Configuration consists of two logical parts: the custom configuration that can be interpreted by the Thing itself and Mainflux-related configuration. Mainflux config contains:
|
||||
1) corresponding Mainflux Thing ID
|
||||
2) corresponding Mainflux Thing key
|
||||
3) list of the Mainflux channels the Thing is connected to
|
||||
|
||||
>Note: list of channels contains IDs of the Mainflux channels. These channels are _pre-provisioned_ on the Mainflux side and, unlike corresponding Mainflux Thing, Bootstrap service is not able to create Mainflux Channels.
|
||||
|
||||
Enabling and disabling Thing (adding Thing to/from whitelist) is as simple as connecting corresponding Mainflux Thing to the given list of Channels. Configuration keeps _state_ of the Thing:
|
||||
|
||||
| State | What it means |
|
||||
|----------|--------------------------------------------------------|
|
||||
| Inactive | Thing is created, but isn't enabled |
|
||||
| Active | Thing is able to communicate using Mainflux |
|
||||
|
||||
Switching between states `Active` and `Inactive` enables and disables Thing, respectively.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|-------------------------------|-------------------------------------------------------------------------|-----------------------|
|
||||
| MF_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | error |
|
||||
| MF_BOOTSTRAP_DB_HOST | Database host address | localhost |
|
||||
| MF_BOOTSTRAP_DB_PORT | Database host port | 5432 |
|
||||
| MF_BOOTSTRAP_DB_USER | Database user | mainflux |
|
||||
| MF_BOOTSTRAP_DB_PASS | Database password | mainflux |
|
||||
| MF_BOOTSTRAP_DB | Name of the database used by the service | bootstrap |
|
||||
| MF_BOOTSTRAP_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MF_BOOTSTRAP_DB_SSL_CERT | Path to the PEM encoded certificate file | |
|
||||
| MF_BOOTSTRAP_DB_SSL_KEY | Path to the PEM encoded key file | |
|
||||
| MF_BOOTSTRAP_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | |
|
||||
| MF_BOOTSTRAP_CLIENT_TLS | Flag that indicates if TLS should be turned on | false |
|
||||
| MF_BOOTSTRAP_CA_CERTS | Path to trusted CAs in PEM format | |
|
||||
| MF_BOOTSTRAP_PORT | Bootstrap service HTTP port | 8180 |
|
||||
| MF_BOOTSTRAP_SERVER_CERT | Path to server certificate in pem format | |
|
||||
| MF_BOOTSTRAP_SERVER_KEY | Path to server key in pem format | |
|
||||
| MF_SDK_BASE_URL | Base url for Mainflux SDK | http://localhost |
|
||||
| MF_SDK_THINGS_PREFIX | SDK prefix for Things service | |
|
||||
| MF_USERS_URL | Users service URL | localhost:8181 |
|
||||
| MF_THINGS_ES_URL | Things service event source URL | localhost:6379 |
|
||||
| MF_THINGS_ES_PASS | Things service event source password | |
|
||||
| MF_THINGS_ES_DB | Things service event source database | 0 |
|
||||
| MF_BOOTSTRAP_INSTANCE_NAME | Bootstrap service instance name | bootstrap |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. The following snippet
|
||||
provides a compose file template that can be used to deploy the service container
|
||||
locally:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
bootstrap:
|
||||
image: mainflux/bootstrap:latest
|
||||
container_name: mainflux-bootstrap
|
||||
depends_on:
|
||||
- bootstrap-db
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 8200:8200
|
||||
environment:
|
||||
MF_BOOTSTRAP_LOG_LEVEL: [Bootstrap log level]
|
||||
MF_BOOTSTRAP_DB_HOST: [Database host address]
|
||||
MF_BOOTSTRAP_DB_PORT: [Database host port]
|
||||
MF_BOOTSTRAP_DB_USER: [Database user]
|
||||
MF_BOOTSTRAP_DB_PASS: [Database password]
|
||||
MF_BOOTSTRAP_DB: [Name of the database used by the service]
|
||||
MF_BOOTSTRAP_DB_SSL_MODE: [SSL mode to connect to the database with]
|
||||
MF_BOOTSTRAP_DB_SSL_CERT: [Path to the PEM encoded certificate file]
|
||||
MF_BOOTSTRAP_DB_SSL_KEY: [Path to the PEM encoded key file]
|
||||
MF_BOOTSTRAP_DB_SSL_ROOT_CERT: [Path to the PEM encoded root certificate file]
|
||||
MF_BOOTSTRAP_CLIENT_TLS: [Boolean value to enable/disable client TLS]
|
||||
MF_BOOTSTRAP_CA_CERTS: [Path to trusted CAs in PEM format]
|
||||
MF_BOOTSTRAP_PORT: 8200
|
||||
MF_BOOTSTRAP_SERVER_CERT: [String path to server cert in pem format]
|
||||
MF_BOOTSTRAP_SERVER_KEY: [String path to server key in pem format]
|
||||
MF_SDK_BASE_URL: [Base SDK URL for the Mainflux services]
|
||||
MF_SDK_THINGS_PREFIX: [SDK prefix for Things service]
|
||||
MF_USERS_URL: [Users service URL]
|
||||
MF_THINGS_ES_URL: [Things service event source URL]
|
||||
MF_THINGS_ES_PASS: [Things service event source password]
|
||||
MF_THINGS_ES_DB: [Things service event source database]
|
||||
MF_BOOTSTRAP_INSTANCE_NAME: [Bootstrap service instance name]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the service
|
||||
make bootstrap
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_BOOTSTRAP_LOG_LEVEL=[Bootstrap log level] MF_BOOTSTRAP_DB_HOST=[Database host address] MF_BOOTSTRAP_DB_PORT=[Database host port] MF_BOOTSTRAP_DB_USER=[Database user] MF_BOOTSTRAP_DB_PASS=[Database password] MF_BOOTSTRAP_DB=[Name of the database used by the service] MF_BOOTSTRAP_DB_SSL_MODE=[SSL mode to connect to the database with] MF_BOOTSTRAP_DB_SSL_CERT=[Path to the PEM encoded certificate file] MF_BOOTSTRAP_DB_SSL_KEY=[Path to the PEM encoded key file] MF_BOOTSTRAP_DB_SSL_ROOT_CERT=[Path to the PEM encoded root certificate file] MF_BOOTSTRAP_CLIENT_TLS=[Boolean value to enable/disable client TLS] MF_BOOTSTRAP_CA_CERTS=[Path to trusted CAs in PEM format] MF_BOOTSTRAP_PORT=[Service HTTP port] MF_BOOTSTRAP_SERVER_CERT=[Path to server certificate] MF_BOOTSTRAP_SERVER_KEY=[Path to server key] MF_SDK_BASE_URL=[Base SDK URL for the Mainflux services] MF_SDK_THINGS_PREFIX=[SDK prefix for Things service] MF_USERS_URL=[Users service URL] $GOBIN/mainflux-bootstrap
|
||||
```
|
||||
|
||||
Setting `MF_BOOTSTRAP_CA_CERTS` expects a file in PEM format of trusted CAs. This will enable TLS against the Users gRPC endpoint trusting only those CAs that are provided.
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
||||
@@ -0,0 +1,9 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Package api contains implementation of things service HTTP API.
|
||||
package api
|
||||
@@ -0,0 +1,241 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(addReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels := []bootstrap.Channel{}
|
||||
for _, c := range req.Channels {
|
||||
channels = append(channels, bootstrap.Channel{ID: c})
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
MFThing: req.ThingID,
|
||||
ExternalID: req.ExternalID,
|
||||
ExternalKey: req.ExternalKey,
|
||||
MFChannels: channels,
|
||||
Name: req.Name,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
saved, err := svc.Add(req.key, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: saved.MFThing,
|
||||
created: true,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(entityReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := svc.View(req.key, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channels []channelRes
|
||||
for _, ch := range config.MFChannels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
res := viewRes{
|
||||
MFThing: config.MFThing,
|
||||
MFKey: config.MFKey,
|
||||
Channels: channels,
|
||||
ExternalID: config.ExternalID,
|
||||
ExternalKey: config.ExternalKey,
|
||||
Name: config.Name,
|
||||
Content: config.Content,
|
||||
State: config.State,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
MFThing: req.id,
|
||||
Name: req.Name,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
if err := svc.Update(req.key, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: config.MFThing,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateConnReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.UpdateConnections(req.key, req.id, req.Channels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: req.id,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
page, err := svc.List(req.key, req.filter, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case req.filter.Unknown:
|
||||
res := listUnknownRes{}
|
||||
for _, cfg := range page.Configs {
|
||||
res.Configs = append(res.Configs, unknownRes{
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
default:
|
||||
res := listRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
Configs: []viewRes{},
|
||||
}
|
||||
|
||||
for _, cfg := range page.Configs {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.MFChannels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
view := viewRes{
|
||||
MFThing: cfg.MFThing,
|
||||
MFKey: cfg.MFKey,
|
||||
Channels: channels,
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Name: cfg.Name,
|
||||
Content: cfg.Content,
|
||||
State: cfg.State,
|
||||
}
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(entityReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return removeRes{}, err
|
||||
}
|
||||
|
||||
if err := svc.Remove(req.key, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(bootstrapReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := svc.Bootstrap(req.key, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reader.ReadConfig(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(changeStateReq)
|
||||
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.ChangeState(req.key, req.id, req.State); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stateRes{}, nil
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,186 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// NewLoggingMiddleware adds logging facilities to the core service.
|
||||
func NewLoggingMiddleware(svc bootstrap.Service, logger log.Logger) bootstrap.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Add(key string, thing bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method add for key %s and thing %s took %s to complete", key, saved.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Add(key, thing)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) View(key, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method view for key %s and thing %s took %s to complete", key, saved.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.View(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Update(key string, thing bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update for key %s and thing %s took %s to complete", key, thing.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Update(key, thing)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateConnections(key, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_connections for key %s and thing %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateConnections(key, id, connections)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) List(key string, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list for key %s and offset %d and limit %d took %s to complete", key, offset, limit, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.List(key, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Remove(key, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove for key %s and thing %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Remove(key, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method bootstrap for thing with external id %s took %s to complete", externalID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Bootstrap(externalKey, externalID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ChangeState(key, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method change_state for key %s and thing %s took %s to complete", key, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ChangeState(key, id, state)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_channel_handler for channel %s took %s to complete", channel.ID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateChannelHandler(channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveConfigHandler(id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_config_handler for config %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveConfigHandler(id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveChannelHandler(id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method remove_channel_handler for channel %s took %s to complete", id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveChannelHandler(id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method disconnect_thing_handler for channel %s and thing %s took %s to complete", channelID, thingID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.DisconnectThingHandler(channelID, thingID)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency metrics.Histogram) bootstrap.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Add(key string, thing bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "add").Add(1)
|
||||
mm.latency.With("method", "add").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Add(key, thing)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) View(id, key string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "view").Add(1)
|
||||
mm.latency.With("method", "view").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.View(id, key)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Update(key string, thing bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update").Add(1)
|
||||
mm.latency.With("method", "update").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Update(key, thing)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateConnections(key, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_connections").Add(1)
|
||||
mm.latency.With("method", "update_connections").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateConnections(key, id, connections)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) List(key string, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list").Add(1)
|
||||
mm.latency.With("method", "list").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.List(key, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Remove(id, key string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove").Add(1)
|
||||
mm.latency.With("method", "remove").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Remove(id, key)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Bootstrap(externalKey, externalID string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "bootstrap").Add(1)
|
||||
mm.latency.With("method", "bootstrap").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Bootstrap(externalKey, externalID)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ChangeState(id, key string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "change_state").Add(1)
|
||||
mm.latency.With("method", "change_state").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.ChangeState(id, key, state)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_channel").Add(1)
|
||||
mm.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateChannelHandler(channel)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) RemoveConfigHandler(id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_config").Add(1)
|
||||
mm.latency.With("method", "remove_config").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveConfigHandler(id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) RemoveChannelHandler(id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_channel").Add(1)
|
||||
mm.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveChannelHandler(id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) DisconnectThingHandler(channelID, thingID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "disconnect_thing_handler").Add(1)
|
||||
mm.latency.With("method", "disconnect_thing_handler").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.DisconnectThingHandler(channelID, thingID)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import "github.com/mainflux/mainflux/bootstrap"
|
||||
|
||||
type apiReq interface {
|
||||
validate() error
|
||||
}
|
||||
|
||||
type addReq struct {
|
||||
key string
|
||||
ThingID string `json:"thing_id"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Channels []string `json:"channels"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (req addReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.ExternalID == "" || req.ExternalKey == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type entityReq struct {
|
||||
key string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req entityReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateReq struct {
|
||||
key string
|
||||
id string
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (req updateReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateConnReq struct {
|
||||
key string
|
||||
id string
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
func (req updateConnReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
key string
|
||||
filter bootstrap.Filter
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req listReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.limit == 0 || req.limit > maxLimit {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type bootstrapReq struct {
|
||||
key string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req bootstrapReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type changeStateReq struct {
|
||||
key string
|
||||
id string
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
||||
func (req changeStateReq) validate() error {
|
||||
if req.key == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
if req.State != bootstrap.Inactive &&
|
||||
req.State != bootstrap.Active {
|
||||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty external ID",
|
||||
key: "key",
|
||||
externalID: "",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
key: "key",
|
||||
externalID: "external-id",
|
||||
externalKey: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := addReq{
|
||||
key: tc.key,
|
||||
ExternalID: tc.externalID,
|
||||
ExternalKey: tc.externalKey,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := entityReq{
|
||||
key: tc.key,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
offset uint64
|
||||
key string
|
||||
limit uint64
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "too large limit",
|
||||
key: "key",
|
||||
offset: 0,
|
||||
limit: maxLimit + 1,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "zero limit",
|
||||
key: "key",
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := listReq{
|
||||
key: tc.key,
|
||||
offset: tc.offset,
|
||||
limit: tc.limit,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
externKey string
|
||||
externID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty external key",
|
||||
externKey: "",
|
||||
externID: "id",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty external id",
|
||||
externKey: "key",
|
||||
externID: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := bootstrapReq{
|
||||
id: tc.externID,
|
||||
key: tc.externKey,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeStateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
id: "id",
|
||||
state: bootstrap.State(1),
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
key: "key",
|
||||
id: "",
|
||||
state: bootstrap.State(0),
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "invalid state",
|
||||
key: "key",
|
||||
id: "id",
|
||||
state: bootstrap.State(14),
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := changeStateReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
State: tc.state,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
var (
|
||||
_ mainflux.Response = (*removeRes)(nil)
|
||||
_ mainflux.Response = (*configRes)(nil)
|
||||
_ mainflux.Response = (*stateRes)(nil)
|
||||
_ mainflux.Response = (*viewRes)(nil)
|
||||
_ mainflux.Response = (*listRes)(nil)
|
||||
)
|
||||
|
||||
type removeRes struct{}
|
||||
|
||||
func (res removeRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type configRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res configRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res configRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/things/configs/%s", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res configRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type viewRes struct {
|
||||
MFThing string `json:"mainflux_id,omitempty"`
|
||||
MFKey string `json:"mainflux_key,omitempty"`
|
||||
Channels []channelRes `json:"mainflux_channels,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
||||
func (res viewRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type unknownRes struct {
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
}
|
||||
|
||||
type listUnknownRes struct {
|
||||
Configs []unknownRes `json:"configs"`
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []viewRes `json:"configs"`
|
||||
}
|
||||
|
||||
func (res listRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type stateRes struct{}
|
||||
|
||||
func (res stateRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res stateRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res stateRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
maxLimit = 100
|
||||
defaultLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
errInvalidQueryParams = errors.New("invalid query params")
|
||||
fullMatch = []string{"state", "external_id", "mainflux_id", "mainflux_key"}
|
||||
partialMatch = []string{"name"}
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
r := bone.New()
|
||||
|
||||
r.Post("/things/configs", kithttp.NewServer(
|
||||
addEndpoint(svc),
|
||||
decodeAddRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/configs/:id", kithttp.NewServer(
|
||||
viewEndpoint(svc),
|
||||
decodeEntityRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Put("/things/configs/:id", kithttp.NewServer(
|
||||
updateEndpoint(svc),
|
||||
decodeUpdateRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Put("/things/configs/connections/:id", kithttp.NewServer(
|
||||
updateConnEndpoint(svc),
|
||||
decodeUpdateConnRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/configs", kithttp.NewServer(
|
||||
listEndpoint(svc),
|
||||
decodeListRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/unknown/configs", kithttp.NewServer(
|
||||
listEndpoint(svc),
|
||||
decodeUnknownRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Get("/things/bootstrap/:external_id", kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader),
|
||||
decodeBootstrapRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Put("/things/state/:id", kithttp.NewServer(
|
||||
stateEndpoint(svc),
|
||||
decodeStateRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.Delete("/things/configs/:id", kithttp.NewServer(
|
||||
removeEndpoint(svc),
|
||||
decodeEntityRequest,
|
||||
encodeResponse,
|
||||
opts...))
|
||||
|
||||
r.GetFunc("/version", mainflux.Version("bootstrap"))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addReq{key: r.Header.Get("Authorization")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateConnReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUnknownRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
|
||||
offset, limit, err := parsePagePrams(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
filter: bootstrap.Filter{Unknown: true},
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, errInvalidQueryParams
|
||||
}
|
||||
|
||||
offset, limit, err := parsePagePrams(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filter := parseFilter(q)
|
||||
|
||||
req := listReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
filter: filter,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := bootstrapReq{
|
||||
id: bone.GetValue(r, "external_id"),
|
||||
key: r.Header.Get("Authorization"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := changeStateReq{key: r.Header.Get("Authorization")}
|
||||
req.id = bone.GetValue(r, "id")
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeEntityRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := entityReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
id: bone.GetValue(r, "id"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if ar, ok := response.(mainflux.Response); ok {
|
||||
for k, v := range ar.Headers() {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
w.WriteHeader(ar.Code())
|
||||
|
||||
if ar.Empty() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
switch err {
|
||||
case errUnsupportedContentType:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case errInvalidQueryParams, bootstrap.ErrMalformedEntity:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case bootstrap.ErrNotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case bootstrap.ErrUnauthorizedAccess:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case bootstrap.ErrConflict:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case bootstrap.ErrThings:
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
case io.EOF:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
switch err.(type) {
|
||||
case *json.SyntaxError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case *json.UnmarshalTypeError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseUint(s string) (uint64, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
ret, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, errInvalidQueryParams
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func parsePagePrams(q url.Values) (uint64, uint64, error) {
|
||||
offset, err := parseUint(q.Get("offset"))
|
||||
q.Del("offset")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
limit, err := parseUint(q.Get("limit"))
|
||||
q.Del("limit")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if limit > maxLimit {
|
||||
limit = maxLimit
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
limit = defaultLimit
|
||||
}
|
||||
|
||||
return offset, limit, nil
|
||||
}
|
||||
|
||||
func parseFilter(values url.Values) bootstrap.Filter {
|
||||
ret := bootstrap.Filter{
|
||||
FullMatch: make(map[string]string),
|
||||
PartialMatch: make(map[string]string),
|
||||
}
|
||||
for k := range values {
|
||||
if contains(fullMatch, k) {
|
||||
ret.FullMatch[k] = values.Get(k)
|
||||
}
|
||||
if contains(partialMatch, k) {
|
||||
ret.PartialMatch[k] = strings.ToLower(values.Get(k))
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func contains(l []string, s string) bool {
|
||||
for _, v := range l {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package bootstrap
|
||||
|
||||
// Config represents Configuration entity. It wraps information about external entity
|
||||
// as well as info about corresponding Mainflux entities.
|
||||
// MFThing represents corresponding Mainflux Thing ID.
|
||||
// MFKey is key of corresponding Mainflux Thing.
|
||||
// MFChannels is a list of Mainflux Channels corresponding Mainflux Thing connects to.
|
||||
type Config struct {
|
||||
MFThing string
|
||||
Owner string
|
||||
Name string
|
||||
MFKey string
|
||||
MFChannels []Channel
|
||||
ExternalID string
|
||||
ExternalKey string
|
||||
Content string
|
||||
State State
|
||||
}
|
||||
|
||||
// Channel represents Mainflux channel corresponding Mainflux Thing is connected to.
|
||||
type Channel struct {
|
||||
ID string
|
||||
Name string
|
||||
Metadata interface{}
|
||||
}
|
||||
|
||||
// Filter is used for the search filters.
|
||||
type Filter struct {
|
||||
Unknown bool
|
||||
FullMatch map[string]string
|
||||
PartialMatch map[string]string
|
||||
}
|
||||
|
||||
// ConfigsPage contains page related metadata as well as list of Configs that
|
||||
// belong to this page.
|
||||
type ConfigsPage struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Configs []Config
|
||||
}
|
||||
|
||||
// ConfigRepository specifies a Config persistence API.
|
||||
type ConfigRepository interface {
|
||||
// Save persists the Config. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
Save(Config, []string) (string, error)
|
||||
|
||||
// RetrieveByID retrieves the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
RetrieveByID(string, string) (Config, error)
|
||||
|
||||
// RetrieveAll retrieves a subset of Configs that are owned
|
||||
// by the specific user, with given filter parameters.
|
||||
RetrieveAll(string, Filter, uint64, uint64) ConfigsPage
|
||||
|
||||
// RetrieveByExternalID returns Config for given external ID.
|
||||
RetrieveByExternalID(string, string) (Config, error)
|
||||
|
||||
// Update performs and update to an existing Config. A non-nil error is returned
|
||||
// to indicate operation failure.
|
||||
Update(Config) error
|
||||
|
||||
// UpdateConnections updates a list of Channels the Config is connected to
|
||||
// adding new Channels if needed.
|
||||
UpdateConnections(string, string, []Channel, []string) error
|
||||
|
||||
// Remove removes the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(string, string) error
|
||||
|
||||
// ChangeState changes of the Config, that is owned by the specific user.
|
||||
ChangeState(string, string, State) error
|
||||
|
||||
// SaveUnknown saves Thing which unsuccessfully bootstrapped.
|
||||
SaveUnknown(string, string) error
|
||||
|
||||
// RetrieveUnknown returns a subset of unsuccessfully bootstrapped Things.
|
||||
RetrieveUnknown(uint64, uint64) ConfigsPage
|
||||
|
||||
// ListExisting retrieves those channels from the given list that exist in DB.
|
||||
ListExisting(string, []string) ([]Channel, error)
|
||||
|
||||
// Methods RemoveThing, UpdateChannel, and RemoveChannel are related to
|
||||
// event sourcing. That's why these methods surpass ownership check.
|
||||
|
||||
// RemoveThing removes Config of the Thing with the given ID.
|
||||
RemoveThing(string) error
|
||||
|
||||
// UpdateChannel updates channel with the given ID.
|
||||
UpdateChannel(Channel) error
|
||||
|
||||
// RemoveChannel removes channel with the given ID.
|
||||
RemoveChannel(string) error
|
||||
|
||||
// DisconnectHandler changes state of the Config when the corresponding Thing is
|
||||
// disconnected from the Channel.
|
||||
DisconnectThing(string, string) error
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Package bootstrap contains the domain concept definitions needed to support
|
||||
// Mainflux bootstrap service functionality.
|
||||
package bootstrap
|
||||
@@ -0,0 +1,346 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
)
|
||||
|
||||
const (
|
||||
emptyState = -1
|
||||
notFoundIdx = -1
|
||||
)
|
||||
|
||||
var _ bootstrap.ConfigRepository = (*configRepositoryMock)(nil)
|
||||
|
||||
type configRepositoryMock struct {
|
||||
mu sync.Mutex
|
||||
counter uint64
|
||||
configs map[string]bootstrap.Config
|
||||
channels map[string]bootstrap.Channel
|
||||
unknown map[string]string
|
||||
}
|
||||
|
||||
// NewConfigsRepository creates in-memory thing repository.
|
||||
func NewConfigsRepository(unknown map[string]string) bootstrap.ConfigRepository {
|
||||
return &configRepositoryMock{
|
||||
configs: make(map[string]bootstrap.Config),
|
||||
channels: make(map[string]bootstrap.Channel),
|
||||
unknown: unknown,
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) Save(config bootstrap.Config, connections []string) (string, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for _, v := range crm.configs {
|
||||
if v.MFThing == config.MFThing || v.ExternalID == config.ExternalID {
|
||||
return "", bootstrap.ErrConflict
|
||||
}
|
||||
}
|
||||
|
||||
crm.counter++
|
||||
config.MFThing = strconv.FormatUint(crm.counter, 10)
|
||||
crm.configs[config.MFThing] = config
|
||||
|
||||
for _, ch := range config.MFChannels {
|
||||
crm.channels[ch.ID] = ch
|
||||
}
|
||||
|
||||
config.MFChannels = []bootstrap.Channel{}
|
||||
|
||||
for _, ch := range connections {
|
||||
config.MFChannels = append(config.MFChannels, crm.channels[ch])
|
||||
}
|
||||
|
||||
crm.configs[config.MFThing] = config
|
||||
delete(crm.unknown, config.ExternalID)
|
||||
|
||||
return config.MFThing, nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveByID(key, id string) (bootstrap.Config, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
c, ok := crm.configs[id]
|
||||
if !ok {
|
||||
return bootstrap.Config{}, bootstrap.ErrNotFound
|
||||
}
|
||||
if c.Owner != key {
|
||||
return bootstrap.Config{}, bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return c, nil
|
||||
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveAll(key string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
configs := make([]bootstrap.Config, 0)
|
||||
|
||||
if offset < 0 || limit <= 0 {
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
first := uint64(offset) + 1
|
||||
last := first + uint64(limit)
|
||||
var state bootstrap.State = emptyState
|
||||
var name string
|
||||
if s, ok := filter.FullMatch["state"]; ok {
|
||||
val, _ := strconv.Atoi(s)
|
||||
state = bootstrap.State(val)
|
||||
}
|
||||
|
||||
if s, ok := filter.PartialMatch["name"]; ok {
|
||||
name = strings.ToLower(s)
|
||||
}
|
||||
|
||||
var total uint64
|
||||
for _, v := range crm.configs {
|
||||
id, _ := strconv.ParseUint(v.MFThing, 10, 64)
|
||||
if (state == emptyState || v.State == state) &&
|
||||
(name == "" || strings.Index(strings.ToLower(v.Name), name) != notFoundIdx) &&
|
||||
v.Owner == key {
|
||||
if id >= first && id < last {
|
||||
configs = append(configs, v)
|
||||
}
|
||||
total++
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(configs, func(i, j int) bool {
|
||||
return configs[i].MFThing < configs[j].MFThing
|
||||
})
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Configs: configs,
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveByExternalID(externalKey, externalID string) (bootstrap.Config, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for _, thing := range crm.configs {
|
||||
if thing.ExternalID == externalID && thing.ExternalKey == externalKey {
|
||||
return thing, nil
|
||||
}
|
||||
}
|
||||
|
||||
return bootstrap.Config{}, bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) Update(config bootstrap.Config) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
cfg, ok := crm.configs[config.MFThing]
|
||||
if !ok || cfg.Owner != config.Owner {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
cfg.Name = config.Name
|
||||
cfg.Content = config.Content
|
||||
crm.configs[config.MFThing] = cfg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) UpdateConnections(key, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
config, ok := crm.configs[id]
|
||||
if !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
for _, ch := range channels {
|
||||
crm.channels[ch.ID] = ch
|
||||
}
|
||||
|
||||
config.MFChannels = []bootstrap.Channel{}
|
||||
for _, conn := range connections {
|
||||
ch, ok := crm.channels[conn]
|
||||
if !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
config.MFChannels = append(config.MFChannels, ch)
|
||||
}
|
||||
crm.configs[id] = config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) Remove(key, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for k, v := range crm.configs {
|
||||
if v.Owner == key && k == id {
|
||||
delete(crm.configs, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) ChangeState(key, id string, state bootstrap.State) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
config, ok := crm.configs[id]
|
||||
if !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
if config.Owner != key {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
config.State = state
|
||||
crm.configs[id] = config
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveUnknown(offset, limit uint64) bootstrap.ConfigsPage {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
configs := []bootstrap.Config{}
|
||||
i := uint64(0)
|
||||
l := int(limit)
|
||||
var keys []string
|
||||
for k := range crm.unknown {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
if i >= offset && len(configs) < l {
|
||||
configs = append(configs, bootstrap.Config{
|
||||
ExternalID: k,
|
||||
ExternalKey: crm.unknown[k],
|
||||
})
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: uint64(len(crm.unknown)),
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Configs: configs,
|
||||
}
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RemoveUnknown(key, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for k, v := range crm.unknown {
|
||||
if k == id && v == key {
|
||||
delete(crm.unknown, k)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) SaveUnknown(key, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
crm.unknown[id] = key
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) ListExisting(key string, connections []string) ([]bootstrap.Channel, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
var ret []bootstrap.Channel
|
||||
|
||||
for k, v := range crm.channels {
|
||||
for _, conn := range connections {
|
||||
if conn == k {
|
||||
ret = append(ret, v)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RemoveThing(id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
delete(crm.configs, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) UpdateChannel(ch bootstrap.Channel) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
channel, ok := crm.channels[ch.ID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
channel.Name = ch.Name
|
||||
channel.Metadata = ch.Metadata
|
||||
crm.channels[ch.ID] = channel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RemoveChannel(id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
delete(crm.channels, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) DisconnectThing(channelID, thingID string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
idx := -1
|
||||
if config, ok := crm.configs[thingID]; ok {
|
||||
for i, ch := range config.MFChannels {
|
||||
if ch.ID == channelID {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if idx != -1 {
|
||||
config.MFChannels = append(config.MFChannels[0:idx], config.MFChannels[idx:]...)
|
||||
}
|
||||
crm.configs[thingID] = config
|
||||
}
|
||||
|
||||
delete(crm.channels, channelID)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
|
||||
var _ things.Service = (*mainfluxThings)(nil)
|
||||
|
||||
type mainfluxThings struct {
|
||||
mu sync.Mutex
|
||||
counter uint64
|
||||
things map[string]things.Thing
|
||||
channels map[string]things.Channel
|
||||
users mainflux.UsersServiceClient
|
||||
connections map[string][]string
|
||||
}
|
||||
|
||||
// NewThingsService returns Mainflux Things service mock.
|
||||
// Only methods used by SDK are mocked.
|
||||
func NewThingsService(things map[string]things.Thing, channels map[string]things.Channel, users mainflux.UsersServiceClient) things.Service {
|
||||
return &mainfluxThings{
|
||||
things: things,
|
||||
channels: channels,
|
||||
users: users,
|
||||
connections: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) AddThing(owner string, thing things.Thing) (things.Thing, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.Thing{}, things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
svc.counter++
|
||||
thing.Owner = userID.Value
|
||||
thing.ID = strconv.FormatUint(svc.counter, 10)
|
||||
thing.Key = thing.ID
|
||||
svc.things[thing.ID] = thing
|
||||
return thing, nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ViewThing(owner, id string) (things.Thing, error) {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.Thing{}, things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if t, ok := svc.things[id]; ok && t.Owner == userID.Value {
|
||||
return t, nil
|
||||
|
||||
}
|
||||
|
||||
return things.Thing{}, things.ErrNotFound
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Connect(owner, chanID, thingID string) error {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if svc.channels[chanID].Owner != userID.Value {
|
||||
return things.ErrNotFound
|
||||
}
|
||||
|
||||
svc.connections[chanID] = append(svc.connections[chanID], thingID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Disconnect(owner, chanID, thingID string) error {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil || svc.channels[chanID].Owner != userID.Value {
|
||||
return things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
ids := svc.connections[chanID]
|
||||
i := 0
|
||||
for _, t := range ids {
|
||||
if t == thingID {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if i == len(ids) {
|
||||
return things.ErrNotFound
|
||||
}
|
||||
|
||||
var tmp []string
|
||||
if i != len(ids)-2 {
|
||||
tmp = ids[i+1:]
|
||||
}
|
||||
ids = append(ids[:i], tmp...)
|
||||
svc.connections[chanID] = ids
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) RemoveThing(owner, id string) error {
|
||||
svc.mu.Lock()
|
||||
defer svc.mu.Unlock()
|
||||
|
||||
userID, err := svc.users.Identify(context.Background(), &mainflux.Token{Value: owner})
|
||||
if err != nil {
|
||||
return things.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if t, ok := svc.things[id]; !ok || t.Owner != userID.Value {
|
||||
return things.ErrNotFound
|
||||
}
|
||||
|
||||
delete(svc.things, id)
|
||||
conns := make(map[string][]string)
|
||||
for k, v := range svc.connections {
|
||||
idx := findIndex(v, id)
|
||||
if idx != -1 {
|
||||
var tmp []string
|
||||
if idx != len(v)-2 {
|
||||
tmp = v[idx+1:]
|
||||
}
|
||||
conns[k] = append(v[:idx], tmp...)
|
||||
}
|
||||
}
|
||||
|
||||
svc.connections = conns
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ViewChannel(owner, id string) (things.Channel, error) {
|
||||
if c, ok := svc.channels[id]; ok {
|
||||
return c, nil
|
||||
}
|
||||
return things.Channel{}, things.ErrNotFound
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) UpdateThing(string, things.Thing) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListThings(string, uint64, uint64) (things.ThingsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListChannelsByThing(string, string, uint64, uint64) (things.ChannelsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListThingsByChannel(string, string, uint64, uint64) (things.ThingsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CreateChannel(string, things.Channel) (things.Channel, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) UpdateChannel(string, things.Channel) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListChannels(string, uint64, uint64) (things.ChannelsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) RemoveChannel(string, string) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) CanAccess(string, string) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) Identify(string) (string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func findIndex(list []string, val string) int {
|
||||
for i, v := range list {
|
||||
if v == val {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var _ mainflux.UsersServiceClient = (*usersServiceMock)(nil)
|
||||
|
||||
type usersServiceMock struct {
|
||||
users map[string]string
|
||||
}
|
||||
|
||||
// NewUsersService creates mock of users service.
|
||||
func NewUsersService(users map[string]string) mainflux.UsersServiceClient {
|
||||
return &usersServiceMock{users}
|
||||
}
|
||||
|
||||
func (svc usersServiceMock) Identify(ctx context.Context, in *mainflux.Token, opts ...grpc.CallOption) (*mainflux.UserID, error) {
|
||||
if id, ok := svc.users[in.Value]; ok {
|
||||
return &mainflux.UserID{Value: id}, nil
|
||||
}
|
||||
return nil, users.ErrUnauthorizedAccess
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
duplicateErr = "unique_violation"
|
||||
uuidErr = "invalid input syntax for type uuid"
|
||||
connConstraintErr = "connections_config_id_fkey"
|
||||
fkViolation = "foreign_key_violation"
|
||||
configFieldsNum = 8
|
||||
chanFieldsNum = 3
|
||||
connFieldsNum = 2
|
||||
cleanupQuery = `DELETE FROM channels ch WHERE NOT EXISTS (
|
||||
SELECT channel_id FROM connections c WHERE ch.mainflux_channel = c.channel_id);`
|
||||
)
|
||||
|
||||
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
|
||||
|
||||
type configRepository struct {
|
||||
db *sql.DB
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewConfigRepository instantiates a PostgreSQL implementation of thing
|
||||
// repository.
|
||||
func NewConfigRepository(db *sql.DB, log logger.Logger) bootstrap.ConfigRepository {
|
||||
return &configRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr configRepository) Save(cfg bootstrap.Config, connections []string) (string, error) {
|
||||
q := `INSERT INTO configs (mainflux_thing, owner, name, mainflux_key, external_id, external_key, content, state)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`
|
||||
|
||||
content := nullString(cfg.Content)
|
||||
name := nullString(cfg.Name)
|
||||
tx, err := cr.db.Begin()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(q, cfg.MFThing, cfg.Owner, name, cfg.MFKey, cfg.ExternalID, cfg.ExternalKey, content, cfg.State); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
e = bootstrap.ErrConflict
|
||||
}
|
||||
|
||||
cr.rollback("Failed to insert a Config", tx, err)
|
||||
|
||||
return "", e
|
||||
}
|
||||
|
||||
if err := insertChannels(cfg.Owner, cfg.MFChannels, tx); err != nil {
|
||||
cr.rollback("Failed to insert Channels", tx, err)
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := insertConnections(cfg, connections, tx); err != nil {
|
||||
cr.rollback("Failed to insert connections", tx, err)
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
q = "DELETE FROM unknown_configs WHERE external_id = $1 AND external_key = $2"
|
||||
|
||||
if _, err := tx.Exec(q, cfg.ExternalID, cfg.ExternalKey); err != nil {
|
||||
cr.rollback("Failed to remove from unknown", tx, err)
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cr.rollback("Failed to commit Config save", tx, err)
|
||||
}
|
||||
|
||||
return cfg.MFThing, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByID(key, id string) (bootstrap.Config, error) {
|
||||
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state FROM configs WHERE mainflux_thing = $1 AND owner = $2`
|
||||
cfg := bootstrap.Config{MFThing: id, Owner: key, MFChannels: []bootstrap.Channel{}}
|
||||
var name, content sql.NullString
|
||||
if err := cr.db.QueryRow(q, id, key).
|
||||
Scan(&cfg.MFThing, &cfg.MFKey, &cfg.ExternalID, &cfg.ExternalKey, &name, &content, &cfg.State); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
return empty, err
|
||||
}
|
||||
|
||||
q = `SELECT mainflux_channel, name, metadata FROM channels ch
|
||||
INNER JOIN connections conn
|
||||
ON ch.mainflux_channel = conn.channel_id AND ch.owner = conn.config_owner
|
||||
WHERE conn.config_id = $1 AND conn.config_owner = $2`
|
||||
|
||||
rows, err := cr.db.Query(q, cfg.MFThing, cfg.Owner)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
c := bootstrap.Channel{}
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Metadata); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
cfg.MFChannels = append(cfg.MFChannels, c)
|
||||
}
|
||||
|
||||
cfg.Content = content.String
|
||||
cfg.Name = name.String
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveAll(key string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
search, params := cr.retrieveAll(key, filter)
|
||||
n := len(params)
|
||||
|
||||
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state
|
||||
FROM configs %s ORDER BY mainflux_thing LIMIT $%d OFFSET $%d`
|
||||
q = fmt.Sprintf(q, search, n+1, n+2)
|
||||
|
||||
rows, err := cr.db.Query(q, append(params, limit, offset)...)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var name, content sql.NullString
|
||||
configs := []bootstrap.Config{}
|
||||
|
||||
for rows.Next() {
|
||||
c := bootstrap.Config{Owner: key}
|
||||
if err := rows.Scan(&c.MFThing, &c.MFKey, &c.ExternalID, &c.ExternalKey, &name, &content, &c.State); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
c.Name = name.String
|
||||
c.Content = content.String
|
||||
configs = append(configs, c)
|
||||
}
|
||||
|
||||
q = fmt.Sprintf(`SELECT COUNT(*) FROM configs %s`, search)
|
||||
|
||||
var total uint64
|
||||
if err := cr.db.QueryRow(q, params...).Scan(&total); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to count configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Configs: configs,
|
||||
}
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByExternalID(externalKey, externalID string) (bootstrap.Config, error) {
|
||||
q := `SELECT mainflux_thing, mainflux_key, owner, name, content, state FROM configs WHERE external_key = $1 AND external_id = $2`
|
||||
cfg := bootstrap.Config{ExternalID: externalID, ExternalKey: externalKey, MFChannels: []bootstrap.Channel{}}
|
||||
var name, content sql.NullString
|
||||
|
||||
if err := cr.db.QueryRow(q, externalKey, externalID).
|
||||
Scan(&cfg.MFThing, &cfg.MFKey, &cfg.Owner, &name, &content, &cfg.State); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, bootstrap.ErrNotFound
|
||||
}
|
||||
return empty, err
|
||||
}
|
||||
|
||||
q = `SELECT mainflux_channel, name, metadata FROM channels ch
|
||||
INNER JOIN connections conn
|
||||
ON ch.mainflux_channel = conn.channel_id AND ch.owner = conn.config_owner
|
||||
WHERE conn.config_id = $1 AND conn.config_owner = $2`
|
||||
|
||||
rows, err := cr.db.Query(q, cfg.MFThing, cfg.Owner)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
c := bootstrap.Channel{}
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Metadata); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
cfg.MFChannels = append(cfg.MFChannels, c)
|
||||
}
|
||||
|
||||
cfg.Content = content.String
|
||||
cfg.Name = name.String
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Update(cfg bootstrap.Config) error {
|
||||
q := `UPDATE configs SET name = $1, content = $2 WHERE mainflux_thing = $3 AND owner = $4`
|
||||
|
||||
content := nullString(cfg.Content)
|
||||
name := nullString(cfg.Name)
|
||||
|
||||
res, err := cr.db.Exec(q, name, content, cfg.MFThing, cfg.Owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateConnections(key, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
tx, err := cr.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := insertChannels(key, channels, tx); err != nil {
|
||||
cr.rollback("Failed to insert Channels during the update", tx, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateConnections(key, id, connections, tx); err != nil {
|
||||
if e, ok := err.(*pq.Error); ok {
|
||||
if e.Code.Name() == fkViolation && e.Constraint == connConstraintErr {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
}
|
||||
cr.rollback("Failed to update connections during the update", tx, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cr.rollback("Failed to commit Config update", tx, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Remove(key, id string) error {
|
||||
q := `DELETE FROM configs WHERE mainflux_thing = $1 AND owner = $2`
|
||||
if _, err := cr.db.Exec(q, id, key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := cr.db.Exec(cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ChangeState(key, id string, state bootstrap.State) error {
|
||||
q := `UPDATE configs SET state = $1 WHERE mainflux_thing = $2 AND owner = $3;`
|
||||
|
||||
res, err := cr.db.Exec(q, state, id, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ListExisting(key string, ids []string) ([]bootstrap.Channel, error) {
|
||||
q := "SELECT mainflux_channel, name, metadata FROM channels WHERE owner = $1 AND mainflux_channel = ANY ($2)"
|
||||
|
||||
rows, err := cr.db.Query(q, key, pq.Array(ids))
|
||||
if err != nil {
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
|
||||
var channels []bootstrap.Channel
|
||||
for rows.Next() {
|
||||
var ch bootstrap.Channel
|
||||
if err := rows.Scan(&ch.ID, &ch.Name, &ch.Metadata); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved channels due to %s", err))
|
||||
return []bootstrap.Channel{}, nil
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) SaveUnknown(key, id string) error {
|
||||
q := `INSERT INTO unknown_configs (external_id, external_key) VALUES ($1, $2)`
|
||||
|
||||
if _, err := cr.db.Exec(q, id, key); err != nil {
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveUnknown(offset, limit uint64) bootstrap.ConfigsPage {
|
||||
q := `SELECT external_id, external_key FROM unknown_configs LIMIT $1 OFFSET $2`
|
||||
rows, err := cr.db.Query(q, limit, offset)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve config due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []bootstrap.Config{}
|
||||
for rows.Next() {
|
||||
c := bootstrap.Config{}
|
||||
if err := rows.Scan(&c.ExternalID, &c.ExternalKey); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
items = append(items, c)
|
||||
}
|
||||
|
||||
q = fmt.Sprintf(`SELECT COUNT(*) FROM unknown_configs`)
|
||||
|
||||
var total uint64
|
||||
if err := cr.db.QueryRow(q).Scan(&total); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to count unknown configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Configs: items,
|
||||
}
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveThing(id string) error {
|
||||
q := `DELETE FROM configs WHERE mainflux_thing = $1`
|
||||
_, err := cr.db.Exec(q, id)
|
||||
|
||||
if _, err := cr.db.Exec(cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateChannel(channel bootstrap.Channel) error {
|
||||
q := `UPDATE channels SET name = $1, metadata = $2 WHERE mainflux_channel = $3`
|
||||
_, err := cr.db.Exec(q, channel.Name, channel.Metadata, channel.ID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveChannel(id string) error {
|
||||
q := `DELETE FROM channels WHERE mainflux_channel = $1`
|
||||
_, err := cr.db.Exec(q, id)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (cr configRepository) DisconnectThing(channelID, thingID string) error {
|
||||
q := `UPDATE configs SET state = $1 WHERE EXISTS (
|
||||
SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
|
||||
_, err := cr.db.Exec(q, bootstrap.Inactive, thingID, channelID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (cr configRepository) retrieveAll(key string, filter bootstrap.Filter) (string, []interface{}) {
|
||||
template := `WHERE owner = $1 %s`
|
||||
params := []interface{}{key}
|
||||
// One empty string so that strings Join works if only one filter is applied.
|
||||
queries := []string{""}
|
||||
// Since key is the first param, start from 2.
|
||||
counter := 2
|
||||
for k, v := range filter.FullMatch {
|
||||
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
|
||||
params = append(params, v)
|
||||
counter++
|
||||
}
|
||||
for k, v := range filter.PartialMatch {
|
||||
queries = append(queries, fmt.Sprintf("LOWER(%s) LIKE '%%' || $%d || '%%'", k, counter))
|
||||
params = append(params, v)
|
||||
counter++
|
||||
}
|
||||
|
||||
f := strings.Join(queries, " AND ")
|
||||
|
||||
return fmt.Sprintf(template, f), params
|
||||
}
|
||||
|
||||
func (cr configRepository) rollback(content string, tx *sql.Tx, err error) {
|
||||
cr.log.Error(fmt.Sprintf("%s %s", content, err))
|
||||
|
||||
if err := tx.Rollback(); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to rollback due to %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
func insertChannels(key string, channels []bootstrap.Channel, tx *sql.Tx) error {
|
||||
if len(channels) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `INSERT INTO channels (mainflux_channel, owner, name, metadata) VALUES `
|
||||
v := []interface{}{key}
|
||||
var vals []string
|
||||
// Since the first value is owner, start with the second one.
|
||||
count := 2
|
||||
for _, ch := range channels {
|
||||
vals = append(vals, fmt.Sprintf("($%d, $1, $%d, $%d)", count, count+1, count+2))
|
||||
v = append(v, ch.ID, ch.Name, ch.Metadata)
|
||||
count += chanFieldsNum
|
||||
}
|
||||
|
||||
q = fmt.Sprintf("%s%s", q, strings.Join(vals, ","))
|
||||
if _, err := tx.Exec(q, v...); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
e = bootstrap.ErrConflict
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertConnections(cfg bootstrap.Config, connections []string, tx *sql.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner) VALUES`
|
||||
v := []interface{}{cfg.MFThing, cfg.Owner}
|
||||
var vals []string
|
||||
|
||||
// Since the first value is Config ID and the second and third
|
||||
// are Config owner, start with the second one.
|
||||
count := 3
|
||||
for _, id := range connections {
|
||||
vals = append(vals, fmt.Sprintf("($1, $%d, $2, $2)", count))
|
||||
v = append(v, id)
|
||||
count++
|
||||
}
|
||||
|
||||
q = fmt.Sprintf("%s%s", q, strings.Join(vals, ","))
|
||||
_, err := tx.Exec(q, v...)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func updateConnections(key, id string, connections []string, tx *sql.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `DELETE FROM connections
|
||||
WHERE config_id = $1 AND config_owner = $2 AND channel_owner = $2
|
||||
AND channel_id NOT IN ($3)`
|
||||
|
||||
v := []interface{}{id, key}
|
||||
v = append(v, pq.Array(connections))
|
||||
|
||||
res, err := tx.Exec(q, v...)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q = `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner) VALUES`
|
||||
v = []interface{}{id, key}
|
||||
var vals []string
|
||||
|
||||
// Since the first value is Config ID and the second is Config
|
||||
// owner, start with the third one.
|
||||
count := 3
|
||||
for _, chID := range connections {
|
||||
vals = append(vals, fmt.Sprintf("($1, $%d, $2, $2)", count))
|
||||
v = append(v, chID)
|
||||
count++
|
||||
}
|
||||
|
||||
// Add connections for current list of channels. Ignore if already exists.
|
||||
q = fmt.Sprintf("%s%s%s", q, strings.Join(vals, ","), "ON CONFLICT (config_id, config_owner, channel_id, channel_owner) DO NOTHING")
|
||||
if _, err := tx.Exec(q, v...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = tx.Exec(cleanupQuery)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func nullString(s string) sql.NullString {
|
||||
if s == "" {
|
||||
return sql.NullString{}
|
||||
}
|
||||
|
||||
return sql.NullString{
|
||||
String: s,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const numConfigs = 10
|
||||
|
||||
var (
|
||||
config = bootstrap.Config{
|
||||
MFThing: "mf-thing",
|
||||
MFKey: "mf-key",
|
||||
ExternalID: "external-id",
|
||||
ExternalKey: "external-key",
|
||||
Owner: "user@email.com",
|
||||
MFChannels: []bootstrap.Channel{
|
||||
bootstrap.Channel{ID: "1", Name: "name 1", Metadata: "{\"meta\":1}"},
|
||||
bootstrap.Channel{ID: "2", Name: "name 2", Metadata: "{\"meta\":2}"},
|
||||
},
|
||||
Content: "content",
|
||||
State: bootstrap.Inactive,
|
||||
}
|
||||
|
||||
channels = []string{"1", "2"}
|
||||
)
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
diff := "different"
|
||||
|
||||
duplicateThing := config
|
||||
duplicateThing.ExternalID = diff
|
||||
duplicateThing.MFKey = diff
|
||||
duplicateThing.MFChannels = []bootstrap.Channel{}
|
||||
|
||||
duplicateExternal := config
|
||||
duplicateExternal.MFThing = diff
|
||||
duplicateExternal.MFKey = diff
|
||||
duplicateExternal.MFChannels = []bootstrap.Channel{}
|
||||
|
||||
duplicateChannels := config
|
||||
duplicateChannels.ExternalID = diff
|
||||
duplicateChannels.MFKey = diff
|
||||
duplicateChannels.MFThing = diff
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save a config",
|
||||
config: config,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Thing ID",
|
||||
config: duplicateThing,
|
||||
connections: nil,
|
||||
err: bootstrap.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same external ID",
|
||||
config: duplicateExternal,
|
||||
connections: nil,
|
||||
err: bootstrap.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Channels",
|
||||
config: duplicateChannels,
|
||||
connections: channels,
|
||||
err: bootstrap.ErrConflict,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Save(tc.config, tc.connections)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByID(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
id := uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
id, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve config",
|
||||
owner: c.Owner,
|
||||
id: id,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve config with wrong owner",
|
||||
owner: "2",
|
||||
id: id,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a non-existing config",
|
||||
owner: c.Owner,
|
||||
id: uuid.NewV4().String(),
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a config with invalid ID",
|
||||
owner: c.Owner,
|
||||
id: "invalid",
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByID(tc.owner, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
for i := 0; i < numConfigs; i++ {
|
||||
c := config
|
||||
// Use UUID to prevent conflict errors.
|
||||
id := uuid.NewV4().String()
|
||||
c.ExternalID = id
|
||||
c.Name = fmt.Sprintf("name %d", i)
|
||||
c.MFThing = id
|
||||
c.MFKey = id
|
||||
|
||||
if i%2 == 0 {
|
||||
c.State = bootstrap.Active
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
c.MFChannels = nil
|
||||
}
|
||||
|
||||
_, err := repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
offset uint64
|
||||
limit uint64
|
||||
filter bootstrap.Filter
|
||||
size int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve all",
|
||||
owner: config.Owner,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: numConfigs,
|
||||
},
|
||||
{
|
||||
desc: "retrieve subset",
|
||||
owner: config.Owner,
|
||||
offset: 5,
|
||||
limit: uint64(numConfigs - 5),
|
||||
size: numConfigs - 5,
|
||||
},
|
||||
{
|
||||
desc: "retrieve wrong owner",
|
||||
owner: "2",
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 0,
|
||||
},
|
||||
{
|
||||
desc: "retrieve all active",
|
||||
owner: config.Owner,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
|
||||
size: numConfigs / 2,
|
||||
},
|
||||
{
|
||||
desc: "retrieve search by name",
|
||||
owner: config.Owner,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
|
||||
size: 1,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ret := repo.RetrieveAll(tc.owner, tc.filter, tc.offset, tc.limit)
|
||||
size := len(ret.Configs)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByExternalID(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
id := uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve with invalid external ID",
|
||||
externalID: strconv.Itoa(numConfigs + 1),
|
||||
externalKey: config.ExternalKey,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with invalid external key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: "invalid",
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with external key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: c.ExternalKey,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByExternalID(tc.externalKey, tc.externalID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
id := uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
c.Content = "new content"
|
||||
c.Name = "new name"
|
||||
|
||||
wrongOwner := c
|
||||
wrongOwner.Owner = "3"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
config bootstrap.Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong owner",
|
||||
config: wrongOwner,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
config: c,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.Update(tc.config)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
id := uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
// Use UUID to prevent conflicts.
|
||||
id = uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
c.MFChannels = []bootstrap.Channel{}
|
||||
_, err = repo.Save(c, []string{channels[0]})
|
||||
require.Nil(t, err, fmt.Sprintf("Saving a config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update connections of non-existing config",
|
||||
key: config.Owner,
|
||||
id: "unknown",
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update connections",
|
||||
key: config.Owner,
|
||||
id: c.MFThing,
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections no channels",
|
||||
key: config.Owner,
|
||||
id: c.MFThing,
|
||||
channels: nil,
|
||||
connections: nil,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.UpdateConnections(tc.key, tc.id, tc.channels, tc.connections)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
id := uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
id, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
// Removal works the same for both existing and non-existing
|
||||
// (removed) config
|
||||
for i := 0; i < 2; i++ {
|
||||
err := repo.Remove(c.Owner, id)
|
||||
require.Nil(t, err, fmt.Sprintf("%d: failed to remove config due to: %s", i, err))
|
||||
|
||||
_, err = repo.RetrieveByID(c.Owner, id)
|
||||
require.Equal(t, bootstrap.ErrNotFound, err, fmt.Sprintf("%d: expected %s got %s", i, bootstrap.ErrNotFound, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
id := uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
saved, err := repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
owner string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "change state with wrong owner",
|
||||
id: saved,
|
||||
owner: "2",
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state with wrong id",
|
||||
id: uuid.NewV4().String(),
|
||||
owner: c.Owner,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state to Active",
|
||||
id: saved,
|
||||
owner: c.Owner,
|
||||
state: bootstrap.Active,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to Inactive",
|
||||
id: saved,
|
||||
owner: c.Owner,
|
||||
state: bootstrap.Inactive,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.ChangeState(tc.owner, tc.id, tc.state)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListExisting(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
id := uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
var chs []bootstrap.Channel
|
||||
for _, ch := range config.MFChannels {
|
||||
ch.Metadata = []byte(ch.Metadata.(string))
|
||||
chs = append(chs, ch)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
connections []string
|
||||
existing []bootstrap.Channel
|
||||
}{
|
||||
{
|
||||
desc: "list all existing channels",
|
||||
key: c.Owner,
|
||||
connections: channels,
|
||||
existing: chs,
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels",
|
||||
key: c.Owner,
|
||||
connections: []string{channels[0], "5"},
|
||||
existing: []bootstrap.Channel{chs[0]},
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels empty",
|
||||
key: c.Owner,
|
||||
connections: []string{"5", "6"},
|
||||
existing: []bootstrap.Channel{},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
existing, err := repo.ListExisting(tc.key, tc.connections)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", tc.desc, err))
|
||||
assert.ElementsMatch(t, tc.existing, existing, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.existing, existing))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveUnknown(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save unknown",
|
||||
externalID: uuid.NewV4().String(),
|
||||
externalKey: uuid.NewV4().String(),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save invalid unknown",
|
||||
externalID: uuid.NewV4().String(),
|
||||
externalKey: "",
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.SaveUnknown(tc.externalKey, tc.externalID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveUnknown(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
|
||||
for i := 0; i < numConfigs; i++ {
|
||||
id := uuid.NewV4().String()
|
||||
repo.SaveUnknown(id, id)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
offset uint64
|
||||
limit uint64
|
||||
size int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve all",
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: numConfigs,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a subset",
|
||||
offset: 5,
|
||||
limit: uint64(numConfigs - 5),
|
||||
size: numConfigs - 5,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ret := repo.RetrieveUnknown(tc.offset, tc.limit)
|
||||
size := len(ret.Configs)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveThing(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
id := uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
saved, err := repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
for i := 0; i < 2; i++ {
|
||||
err := repo.RemoveThing(saved)
|
||||
assert.Nil(t, err, fmt.Sprintf("an unexpected error occured: %s\n", err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
id := uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
id = c.MFChannels[0].ID
|
||||
update := bootstrap.Channel{
|
||||
ID: id,
|
||||
Name: "update name",
|
||||
Metadata: []byte("{\"update\":\"metadata update\"}"),
|
||||
}
|
||||
err = repo.UpdateChannel(update)
|
||||
assert.Nil(t, err, fmt.Sprintf("updating config expected to succeed: %s.\n", err))
|
||||
|
||||
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
|
||||
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
var retreved bootstrap.Channel
|
||||
for _, c := range cfg.MFChannels {
|
||||
if c.ID == id {
|
||||
retreved = c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, update, retreved, fmt.Sprintf("expected %s, go %s", update, retreved))
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
for i, ch := range c.MFChannels {
|
||||
c.MFChannels[i].Metadata = []byte(ch.Metadata.(string))
|
||||
|
||||
}
|
||||
// Use UUID to prevent conflicts.
|
||||
id := uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
_, err = repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
err = repo.RemoveChannel(c.MFChannels[0].ID)
|
||||
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
|
||||
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
|
||||
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.NotContains(t, cfg.MFChannels, c.MFChannels[0], fmt.Sprintf("expected to remove channel %s from %s", c.MFChannels[0], cfg.MFChannels))
|
||||
}
|
||||
|
||||
func TestDisconnectThing(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
id := uuid.NewV4().String()
|
||||
c.MFKey = id
|
||||
c.MFThing = id
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
saved, err := repo.Save(c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
err = repo.DisconnectThing(c.MFChannels[0].ID, saved)
|
||||
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
|
||||
cfg, err := repo.RetrieveByID(c.Owner, c.MFThing)
|
||||
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, cfg.State, bootstrap.Inactive, fmt.Sprintf("expected ti be inactive when a connection is removed from %s", cfg))
|
||||
}
|
||||
|
||||
func deleteChannels(repo bootstrap.ConfigRepository) error {
|
||||
for _, ch := range channels {
|
||||
if err := repo.RemoveChannel(ch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/lib/pq" // required for SQL access
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
// Config defines the options that are used when connecting to a PostgreSQL instance
|
||||
type Config struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Pass string
|
||||
Name string
|
||||
SSLMode string
|
||||
SSLCert string
|
||||
SSLKey string
|
||||
SSLRootCert string
|
||||
}
|
||||
|
||||
// Connect creates a connection to the PostgreSQL instance and applies any
|
||||
// unapplied database migrations. A non-nil error is returned to indicate
|
||||
// failure.
|
||||
func Connect(cfg Config) (*sql.DB, error) {
|
||||
url := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", cfg.Host, cfg.Port, cfg.User, cfg.Name, cfg.Pass, cfg.SSLMode, cfg.SSLCert, cfg.SSLKey, cfg.SSLRootCert)
|
||||
|
||||
db, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := migrateDB(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrateDB(db *sql.DB) error {
|
||||
migrations := &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "configs_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS configs (
|
||||
mainflux_thing TEXT UNIQUE NOT NULL,
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
mainflux_key CHAR(36) UNIQUE NOT NULL,
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
external_key TEXT NOT NULL,
|
||||
content TEXT,
|
||||
state BIGINT NOT NULL,
|
||||
PRIMARY KEY (mainflux_thing, owner)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS unknown_configs (
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
external_key TEXT NOT NULL,
|
||||
PRIMARY KEY (external_id, external_key)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS channels (
|
||||
mainflux_channel TEXT UNIQUE NOT NULL,
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
metadata JSON,
|
||||
PRIMARY KEY (mainflux_channel, owner)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS connections (
|
||||
channel_id TEXT,
|
||||
channel_owner VARCHAR(256),
|
||||
config_id TEXT,
|
||||
config_owner VARCHAR(256),
|
||||
FOREIGN KEY (channel_id, channel_owner) REFERENCES channels (mainflux_channel, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (config_id, config_owner) REFERENCES configs (mainflux_thing, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (channel_id, channel_owner, config_id, config_owner)
|
||||
)`,
|
||||
},
|
||||
Down: []string{
|
||||
"DROP TABLE connections",
|
||||
"DROP TABLE configs",
|
||||
"DROP TABLE channels",
|
||||
"DROP TABLE unknown_configs",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := migrate.Exec(db, "postgres", migrations, migrate.Up)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"gopkg.in/ory-am/dockertest.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
wrongID = "0"
|
||||
wrongValue = "wrong-value"
|
||||
)
|
||||
|
||||
var (
|
||||
testLog, _ = logger.New(os.Stdout, logger.Info.String())
|
||||
db *sql.DB
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
cfg := []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.2-alpine", cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err = sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
User: "test",
|
||||
Pass: "test",
|
||||
Name: "test",
|
||||
SSLMode: "disable",
|
||||
SSLCert: "",
|
||||
SSLKey: "",
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
if db, err = postgres.Connect(dbConfig); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
// bootstrapRes represent Mainflux Response to the Bootatrap request.
|
||||
// This is used as a response from ConfigReader and can easily be
|
||||
// replace with any other response format.
|
||||
type bootstrapRes struct {
|
||||
MFThing string `json:"mainflux_id"`
|
||||
MFKey string `json:"mainflux_key"`
|
||||
MFChannels []channelRes `json:"mainflux_channels"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type reader struct{}
|
||||
|
||||
// NewConfigReader return new reader which is used to generate response
|
||||
// from the config.
|
||||
func NewConfigReader() ConfigReader {
|
||||
return reader{}
|
||||
}
|
||||
|
||||
func (r reader) ReadConfig(cfg Config) (mainflux.Response, error) {
|
||||
if len(cfg.MFChannels) < 1 {
|
||||
return bootstrapRes{}, errors.New("Invalid configuration")
|
||||
}
|
||||
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.MFChannels {
|
||||
channels = append(channels, channelRes{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
|
||||
}
|
||||
|
||||
res := bootstrapRes{
|
||||
MFKey: cfg.MFKey,
|
||||
MFThing: cfg.MFThing,
|
||||
MFChannels: channels,
|
||||
Content: cfg.Content,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package redis
|
||||
|
||||
type removeEvent struct {
|
||||
id string
|
||||
}
|
||||
|
||||
type updateChannelEvent struct {
|
||||
id string
|
||||
name string
|
||||
metadata string
|
||||
}
|
||||
|
||||
// Connection event is either connect or disconnect event.
|
||||
type disconnectEvent struct {
|
||||
thingID string
|
||||
channelID string
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
stream = "mainflux.things"
|
||||
group = "mainflux.bootstrap"
|
||||
|
||||
thingPrefix = "thing."
|
||||
thingRemove = thingPrefix + "remove"
|
||||
thingDisconnect = thingPrefix + "disconnect"
|
||||
|
||||
channelPrefix = "channel."
|
||||
channelUpdate = channelPrefix + "update"
|
||||
channelRemove = channelPrefix + "remove"
|
||||
|
||||
exists = "BUSYGROUP Consumer Group name already exists"
|
||||
)
|
||||
|
||||
// EventStore represents event source for things and channels provisioning.
|
||||
type EventStore interface {
|
||||
// Subscribes to given subject and receives events.
|
||||
Subscribe(string) error
|
||||
}
|
||||
|
||||
type eventStore struct {
|
||||
svc bootstrap.Service
|
||||
client *redis.Client
|
||||
consumer string
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewEventStore returns new event store instance.
|
||||
func NewEventStore(svc bootstrap.Service, client *redis.Client, consumer string, log logger.Logger) EventStore {
|
||||
return eventStore{
|
||||
svc: svc,
|
||||
client: client,
|
||||
consumer: consumer,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (es eventStore) Subscribe(subject string) error {
|
||||
err := es.client.XGroupCreateMkStream(stream, group, "$").Err()
|
||||
if err != nil && err.Error() != exists {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
streams, err := es.client.XReadGroup(&redis.XReadGroupArgs{
|
||||
Group: group,
|
||||
Consumer: es.consumer,
|
||||
Streams: []string{stream, ">"},
|
||||
Count: 100,
|
||||
}).Result()
|
||||
if err != nil || len(streams) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, msg := range streams[0].Messages {
|
||||
event := msg.Values
|
||||
|
||||
var err error
|
||||
switch event["operation"] {
|
||||
case thingRemove:
|
||||
rte := decodeRemoveThing(event)
|
||||
err = es.handleRemoveThing(rte)
|
||||
case thingDisconnect:
|
||||
dte := decodeDisconnectThing(event)
|
||||
err = es.handleDisconnectThing(dte)
|
||||
case channelUpdate:
|
||||
uce := decodeUpdateChannel(event)
|
||||
err = es.handleUpdateChannel(uce)
|
||||
case channelRemove:
|
||||
rce := decodeRemoveChannel(event)
|
||||
err = es.handleRemoveChannel(rce)
|
||||
}
|
||||
if err != nil {
|
||||
es.logger.Warn(fmt.Sprintf("Failed to handle event sourcing: %s", err.Error()))
|
||||
break
|
||||
}
|
||||
es.client.XAck(stream, group, msg.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRemoveThing(event map[string]interface{}) removeEvent {
|
||||
return removeEvent{
|
||||
id: read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeUpdateChannel(event map[string]interface{}) updateChannelEvent {
|
||||
return updateChannelEvent{
|
||||
id: read(event, "id", ""),
|
||||
name: read(event, "name", ""),
|
||||
metadata: read(event, "metadata", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRemoveChannel(event map[string]interface{}) removeEvent {
|
||||
return removeEvent{
|
||||
id: read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeDisconnectThing(event map[string]interface{}) disconnectEvent {
|
||||
return disconnectEvent{
|
||||
channelID: read(event, "chan_id", ""),
|
||||
thingID: read(event, "thing_id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func (es eventStore) handleRemoveThing(rte removeEvent) error {
|
||||
return es.svc.RemoveConfigHandler(rte.id)
|
||||
}
|
||||
|
||||
func (es eventStore) handleUpdateChannel(uce updateChannelEvent) error {
|
||||
channel := bootstrap.Channel{
|
||||
ID: uce.id,
|
||||
Name: uce.name,
|
||||
Metadata: uce.metadata,
|
||||
}
|
||||
return es.svc.UpdateChannelHandler(channel)
|
||||
}
|
||||
|
||||
func (es eventStore) handleRemoveChannel(rce removeEvent) error {
|
||||
return es.svc.RemoveChannelHandler(rce.id)
|
||||
}
|
||||
|
||||
func (es eventStore) handleDisconnectThing(dte disconnectEvent) error {
|
||||
return es.svc.DisconnectThingHandler(dte.channelID, dte.thingID)
|
||||
}
|
||||
|
||||
func read(event map[string]interface{}, key, def string) string {
|
||||
val, ok := event[key].(string)
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
)
|
||||
|
||||
const (
|
||||
thingType = "device"
|
||||
chanName = "channel"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound indicates a non-existent entity request.
|
||||
ErrNotFound = errors.New("non-existent entity")
|
||||
|
||||
// ErrMalformedEntity indicates malformed entity specification.
|
||||
ErrMalformedEntity = errors.New("malformed entity specification")
|
||||
|
||||
// ErrUnauthorizedAccess indicates missing or invalid credentials provided
|
||||
// when accessing a protected resource.
|
||||
ErrUnauthorizedAccess = errors.New("missing or invalid credentials provided")
|
||||
|
||||
// ErrConflict indicates that entity with the same ID or external ID already exists.
|
||||
ErrConflict = errors.New("entity already exists")
|
||||
|
||||
// ErrThings indicates failure to communicate with Mainflux Things service.
|
||||
// It can be due to networking error or invalid/unauthorized request.
|
||||
ErrThings = errors.New("error receiving response from Things service")
|
||||
)
|
||||
|
||||
var _ Service = (*bootstrapService)(nil)
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// Add adds new Thing Config to the user identified by the provided key.
|
||||
Add(string, Config) (Config, error)
|
||||
|
||||
// View returns Thing Config with given ID belonging to the user identified by the given key.
|
||||
View(string, string) (Config, error)
|
||||
|
||||
// Update updates editable fields of the provided Config.
|
||||
Update(string, Config) error
|
||||
|
||||
// UpdateConnections updates list of Channels related to given Config.
|
||||
UpdateConnections(string, string, []string) error
|
||||
|
||||
// List returns subset of Configs with given search params that belong to the
|
||||
// user identified by the given key.
|
||||
List(string, Filter, uint64, uint64) (ConfigsPage, error)
|
||||
|
||||
// Remove removes Config with specified key that belongs to the user identified by the given key.
|
||||
Remove(string, string) error
|
||||
|
||||
// Bootstrap returns Config to the Thing with provided external ID using external key.
|
||||
Bootstrap(string, string) (Config, error)
|
||||
|
||||
// ChangeState changes state of the Thing with given ID and owner.
|
||||
ChangeState(string, string, State) error
|
||||
|
||||
// Methods RemoveConfig, UpdateChannel, and RemoveChannel are used as
|
||||
// handlers for events. That's why these methods surpass ownership check.
|
||||
|
||||
// RemoveConfigHandler removes Configuration with id received from an event.
|
||||
RemoveConfigHandler(string) error
|
||||
|
||||
// UpdateChannelHandler updates Channel with data received from an event.
|
||||
UpdateChannelHandler(Channel) error
|
||||
|
||||
// RemoveChannelHandler removes Channel with id received from an event.
|
||||
RemoveChannelHandler(string) error
|
||||
|
||||
// DisconnectHandler changes state of the Config when connect/disconnect event occurs.
|
||||
DisconnectThingHandler(string, string) error
|
||||
}
|
||||
|
||||
// ConfigReader is used to parse Config into format which will be encoded
|
||||
// as a JSON and consumed from the client side. The purpose of this interface
|
||||
// is to provide convenient way to generate custom configuration response
|
||||
// based on the specific Config which will be consumed by the client.
|
||||
type ConfigReader interface {
|
||||
ReadConfig(Config) (mainflux.Response, error)
|
||||
}
|
||||
|
||||
type bootstrapService struct {
|
||||
users mainflux.UsersServiceClient
|
||||
configs ConfigRepository
|
||||
sdk mfsdk.SDK
|
||||
}
|
||||
|
||||
// New returns new Bootstrap service.
|
||||
func New(users mainflux.UsersServiceClient, configs ConfigRepository, sdk mfsdk.SDK) Service {
|
||||
return &bootstrapService{
|
||||
configs: configs,
|
||||
sdk: sdk,
|
||||
users: users,
|
||||
}
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Add(key string, cfg Config) (Config, error) {
|
||||
owner, err := bs.identify(key)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
toConnect := bs.toIDList(cfg.MFChannels)
|
||||
|
||||
// Check if channels exist. This is the way to prevent invalid configuration to be saved.
|
||||
existing, err := bs.configs.ListExisting(owner, toConnect)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
cfg.MFChannels, err = bs.connectionChannels(toConnect, bs.toIDList(existing), key)
|
||||
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
id := cfg.MFThing
|
||||
mfThing, err := bs.thing(key, id)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
cfg.MFThing = mfThing.ID
|
||||
cfg.Owner = owner
|
||||
cfg.State = Inactive
|
||||
cfg.MFKey = mfThing.Key
|
||||
saved, err := bs.configs.Save(cfg, toConnect)
|
||||
|
||||
if err != nil {
|
||||
if id == "" {
|
||||
// Fail silently.
|
||||
bs.sdk.DeleteThing(cfg.MFThing, key)
|
||||
}
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
cfg.MFThing = saved
|
||||
cfg.MFChannels = append(cfg.MFChannels, existing...)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) View(key, id string) (Config, error) {
|
||||
owner, err := bs.identify(key)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
cfg, err := bs.configs.RetrieveByID(owner, id)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
for i, ch := range cfg.MFChannels {
|
||||
if meta, ok := ch.Metadata.([]byte); ok {
|
||||
if err := json.Unmarshal(meta, &cfg.MFChannels[i].Metadata); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Update(key string, cfg Config) error {
|
||||
owner, err := bs.identify(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Owner = owner
|
||||
|
||||
return bs.configs.Update(cfg)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateConnections(key, id string, connections []string) error {
|
||||
owner, err := bs.identify(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := bs.configs.RetrieveByID(owner, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
add, remove := bs.updateList(cfg, connections)
|
||||
channels, err := bs.updateChannels(add, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.MFChannels = channels
|
||||
var connect, disconnect []string
|
||||
|
||||
if cfg.State == Active {
|
||||
connect = add
|
||||
disconnect = remove
|
||||
}
|
||||
|
||||
for _, c := range disconnect {
|
||||
if err := bs.sdk.DisconnectThing(id, c, key); err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
continue
|
||||
}
|
||||
return ErrThings
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range connect {
|
||||
if err := bs.sdk.ConnectThing(id, c, key); err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
return ErrMalformedEntity
|
||||
}
|
||||
return ErrThings
|
||||
}
|
||||
}
|
||||
|
||||
return bs.configs.UpdateConnections(owner, id, channels, connections)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) List(key string, filter Filter, offset, limit uint64) (ConfigsPage, error) {
|
||||
owner, err := bs.identify(key)
|
||||
if err != nil {
|
||||
return ConfigsPage{}, err
|
||||
}
|
||||
|
||||
if filter.Unknown {
|
||||
return bs.configs.RetrieveUnknown(offset, limit), nil
|
||||
}
|
||||
|
||||
return bs.configs.RetrieveAll(owner, filter, offset, limit), nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Remove(key, id string) error {
|
||||
owner, err := bs.identify(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bs.configs.Remove(owner, id)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Bootstrap(externalKey, externalID string) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByExternalID(externalKey, externalID)
|
||||
if err != nil {
|
||||
if err == ErrNotFound {
|
||||
bs.configs.SaveUnknown(externalKey, externalID)
|
||||
}
|
||||
return Config{}, ErrNotFound
|
||||
}
|
||||
|
||||
for i, ch := range cfg.MFChannels {
|
||||
if meta, ok := ch.Metadata.([]byte); ok {
|
||||
if err := json.Unmarshal(meta, &cfg.MFChannels[i].Metadata); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ChangeState(key, id string, state State) error {
|
||||
owner, err := bs.identify(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
thing, err := bs.configs.RetrieveByID(owner, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if thing.State == state {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch state {
|
||||
case Active:
|
||||
for _, c := range thing.MFChannels {
|
||||
if err := bs.sdk.ConnectThing(thing.MFThing, c.ID, key); err != nil {
|
||||
return ErrThings
|
||||
}
|
||||
}
|
||||
case Inactive:
|
||||
for _, c := range thing.MFChannels {
|
||||
if err := bs.sdk.DisconnectThing(thing.MFThing, c.ID, key); err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
continue
|
||||
}
|
||||
return ErrThings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bs.configs.ChangeState(owner, id, state)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateChannelHandler(channel Channel) error {
|
||||
return bs.configs.UpdateChannel(channel)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveConfigHandler(id string) error {
|
||||
return bs.configs.RemoveThing(id)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveChannelHandler(id string) error {
|
||||
return bs.configs.RemoveChannel(id)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) DisconnectThingHandler(channelID, thingID string) error {
|
||||
return bs.configs.DisconnectThing(channelID, thingID)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) identify(token string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := bs.users.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return "", ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return res.GetValue(), nil
|
||||
}
|
||||
|
||||
// Method thing retrieves Mainflux Thing creating one if an empty ID is passed.
|
||||
func (bs bootstrapService) thing(key, id string) (mfsdk.Thing, error) {
|
||||
thingID := id
|
||||
var err error
|
||||
|
||||
if id == "" {
|
||||
thingID, err = bs.sdk.CreateThing(mfsdk.Thing{Type: thingType}, key)
|
||||
if err != nil {
|
||||
return mfsdk.Thing{}, err
|
||||
}
|
||||
}
|
||||
|
||||
thing, err := bs.sdk.Thing(thingID, key)
|
||||
if err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
return mfsdk.Thing{}, ErrNotFound
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
bs.sdk.DeleteThing(thingID, key)
|
||||
}
|
||||
|
||||
return mfsdk.Thing{}, ErrThings
|
||||
}
|
||||
|
||||
return thing, nil
|
||||
}
|
||||
|
||||
// Method updateList accepts config and channel IDs and returns three lists:
|
||||
// 1) IDs of Channels to be added
|
||||
// 2) IDs of Channels to be removed
|
||||
// 3) IDs of common Channels for these two configs
|
||||
func (bs bootstrapService) updateList(cfg Config, connections []string) (add, remove []string) {
|
||||
var disconnect map[string]bool
|
||||
disconnect = make(map[string]bool, len(cfg.MFChannels))
|
||||
for _, c := range cfg.MFChannels {
|
||||
disconnect[c.ID] = true
|
||||
}
|
||||
|
||||
for _, c := range connections {
|
||||
if disconnect[c] {
|
||||
// Don't disconnect common elements.
|
||||
delete(disconnect, c)
|
||||
continue
|
||||
}
|
||||
// Connect new elements.
|
||||
add = append(add, c)
|
||||
}
|
||||
|
||||
for v := range disconnect {
|
||||
remove = append(remove, v)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (bs bootstrapService) toIDList(channels []Channel) []string {
|
||||
var ret []string
|
||||
for _, ch := range channels {
|
||||
ret = append(ret, ch.ID)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (bs bootstrapService) updateChannels(add []string, key string) ([]Channel, error) {
|
||||
var ret []Channel
|
||||
for _, id := range add {
|
||||
ch, err := bs.sdk.Channel(id, key)
|
||||
if err != nil {
|
||||
return []Channel{}, ErrMalformedEntity
|
||||
}
|
||||
|
||||
ret = append(ret, Channel{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) connectionChannels(channels, existing []string, key string) ([]Channel, error) {
|
||||
add := make(map[string]bool, len(channels))
|
||||
for _, ch := range channels {
|
||||
add[ch] = true
|
||||
}
|
||||
|
||||
for _, ch := range existing {
|
||||
if add[ch] == true {
|
||||
delete(add, ch)
|
||||
}
|
||||
}
|
||||
|
||||
var ret []Channel
|
||||
for id := range add {
|
||||
ch, err := bs.sdk.Channel(id, key)
|
||||
if err != nil {
|
||||
return nil, ErrMalformedEntity
|
||||
}
|
||||
|
||||
ret = append(ret, Channel{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package bootstrap_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/http"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
validToken = "validToken"
|
||||
invalidToken = "invalidToken"
|
||||
email = "test@example.com"
|
||||
unknown = "unknown"
|
||||
unknownID = "1"
|
||||
unknownKey = "2"
|
||||
channelsNum = 3
|
||||
)
|
||||
|
||||
var (
|
||||
channel = bootstrap.Channel{
|
||||
ID: "1",
|
||||
Name: "name",
|
||||
Metadata: `{"name":"value"}`,
|
||||
}
|
||||
|
||||
config = bootstrap.Config{
|
||||
ExternalID: "external_id",
|
||||
ExternalKey: "external_key",
|
||||
MFChannels: []bootstrap.Channel{channel},
|
||||
Content: "config",
|
||||
}
|
||||
)
|
||||
|
||||
func newService(users mainflux.UsersServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
return bootstrap.New(users, things, sdk)
|
||||
}
|
||||
|
||||
func newThingsService(users mainflux.UsersServiceClient) things.Service {
|
||||
channels := make(map[string]things.Channel, channelsNum)
|
||||
for i := 0; i < channelsNum; i++ {
|
||||
id := strconv.Itoa(i + 1)
|
||||
channels[id] = things.Channel{
|
||||
ID: id,
|
||||
Owner: email,
|
||||
Metadata: `{"meta":"data"}`,
|
||||
}
|
||||
}
|
||||
|
||||
return mocks.NewThingsService(map[string]things.Thing{}, channels, users)
|
||||
}
|
||||
|
||||
func newThingsServer(svc things.Service) *httptest.Server {
|
||||
mux := httpapi.MakeHandler(svc)
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
neID := config
|
||||
neID.MFThing = "non-existent"
|
||||
|
||||
wrongChannels := config
|
||||
ch := channel
|
||||
ch.ID = "invalid"
|
||||
wrongChannels.MFChannels = append(wrongChannels.MFChannels, ch)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "add a new config",
|
||||
config: config,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "add a config with an invalid ID",
|
||||
config: neID,
|
||||
key: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "add a config with wrong credentials",
|
||||
config: config,
|
||||
key: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "add a config with invalid list of channels",
|
||||
config: wrongChannels,
|
||||
key: validToken,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Add(tc.key, tc.config)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestView(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "view an existing config",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "view a non-existing config",
|
||||
id: unknown,
|
||||
key: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "view a config with wrong credentials",
|
||||
id: config.MFThing,
|
||||
key: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.View(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
c := config
|
||||
|
||||
ch := channel
|
||||
ch.ID = "2"
|
||||
c.MFChannels = append(c.MFChannels, ch)
|
||||
saved, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
modifiedCreated := saved
|
||||
modifiedCreated.Content = "new-config"
|
||||
modifiedCreated.Name = "new name"
|
||||
|
||||
nonExisting := config
|
||||
nonExisting.MFThing = unknown
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update a config with state Created",
|
||||
config: modifiedCreated,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update a non-existing config",
|
||||
config: nonExisting,
|
||||
key: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config with wrong credentials",
|
||||
config: saved,
|
||||
key: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Update(tc.key, tc.config)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
c := config
|
||||
|
||||
ch := channel
|
||||
ch.ID = "2"
|
||||
c.MFChannels = append(c.MFChannels, ch)
|
||||
created, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
c.ExternalID = uuid.NewV4().String()
|
||||
active, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
err = svc.ChangeState(validToken, active.MFThing, bootstrap.Active)
|
||||
require.Nil(t, err, fmt.Sprintf("Changing state expected to succeed: %s.\n", err))
|
||||
|
||||
nonExisting := config
|
||||
nonExisting.MFThing = unknown
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
id string
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update connections for config with state Created",
|
||||
key: validToken,
|
||||
id: created.MFThing,
|
||||
connections: []string{"2"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections for config with state Active",
|
||||
key: validToken,
|
||||
id: active.MFThing,
|
||||
connections: []string{"3"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections for non-existing config",
|
||||
key: validToken,
|
||||
id: "",
|
||||
connections: []string{"3"},
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update connections with invalid channels",
|
||||
key: validToken,
|
||||
id: created.MFThing,
|
||||
connections: []string{"wrong"},
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "update connections a config with wrong credentials",
|
||||
key: invalidToken,
|
||||
id: created.MFKey,
|
||||
connections: []string{"2", "3"},
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateConnections(tc.key, tc.id, tc.connections)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
numThings := 101
|
||||
var saved []bootstrap.Config
|
||||
for i := 0; i < numThings; i++ {
|
||||
c := config
|
||||
id := uuid.NewV4().String()
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
c.Name = fmt.Sprintf("%s-%d", config.Name, i)
|
||||
s, err := svc.Add(validToken, c)
|
||||
saved = append(saved, s)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
}
|
||||
// Set one Thing to the different state
|
||||
err := svc.ChangeState(validToken, "42", bootstrap.Active)
|
||||
require.Nil(t, err, fmt.Sprintf("Changing config state expected to succeed: %s.\n", err))
|
||||
saved[41].State = bootstrap.Active
|
||||
|
||||
unknownConfig := bootstrap.Config{
|
||||
ExternalID: unknownID,
|
||||
ExternalKey: unknownKey,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.ConfigsPage
|
||||
filter bootstrap.Filter
|
||||
offset uint64
|
||||
limit uint64
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list configs",
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: uint64(len(saved)),
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Configs: saved[0:10],
|
||||
},
|
||||
filter: bootstrap.Filter{},
|
||||
key: validToken,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list configs with specified name",
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: 1,
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Configs: saved[95:96],
|
||||
},
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "95"}},
|
||||
key: validToken,
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list configs unauthorized",
|
||||
config: bootstrap.ConfigsPage{},
|
||||
filter: bootstrap.Filter{},
|
||||
key: invalidToken,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "list last page",
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: uint64(len(saved)),
|
||||
Offset: 95,
|
||||
Limit: 10,
|
||||
Configs: saved[95:],
|
||||
},
|
||||
filter: bootstrap.Filter{},
|
||||
key: validToken,
|
||||
offset: 95,
|
||||
limit: 10,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list configs with Active state",
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: 1,
|
||||
Offset: 35,
|
||||
Limit: 20,
|
||||
Configs: []bootstrap.Config{saved[41]},
|
||||
},
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
|
||||
key: validToken,
|
||||
offset: 35,
|
||||
limit: 20,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list unknown configs",
|
||||
config: bootstrap.ConfigsPage{
|
||||
Total: 1,
|
||||
Offset: 0,
|
||||
Limit: 20,
|
||||
Configs: []bootstrap.Config{unknownConfig},
|
||||
},
|
||||
filter: bootstrap.Filter{Unknown: true},
|
||||
key: validToken,
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
result, err := svc.List(tc.key, tc.filter, tc.offset, tc.limit)
|
||||
assert.ElementsMatch(t, tc.config.Configs, result.Configs, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Configs, result.Configs))
|
||||
assert.Equal(t, tc.config.Total, result.Total, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.config.Total, result.Total))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "view a config with wrong credentials",
|
||||
id: saved.MFThing,
|
||||
key: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "remove an existing config",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove removed config",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove non-existing config",
|
||||
id: unknown,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Remove(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
externalKey string
|
||||
externalID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "bootstrap using invalid external id",
|
||||
config: bootstrap.Config{},
|
||||
externalID: "invalid",
|
||||
externalKey: saved.ExternalKey,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap using invalid external key",
|
||||
config: bootstrap.Config{},
|
||||
externalID: saved.ExternalID,
|
||||
externalKey: "invalid",
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap an existing config",
|
||||
config: saved,
|
||||
externalID: saved.ExternalID,
|
||||
externalKey: saved.ExternalKey,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
config, err := svc.Bootstrap(tc.externalKey, tc.externalID)
|
||||
assert.Equal(t, tc.config, config, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.config, config))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
state bootstrap.State
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "change state with wrong credentials",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
key: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "change state of non-existing config",
|
||||
state: bootstrap.Active,
|
||||
id: unknown,
|
||||
key: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state to Active",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to current state",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to Inactive",
|
||||
state: bootstrap.Inactive,
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.ChangeState(tc.key, tc.id, tc.state)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannelHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
_, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
ch := bootstrap.Channel{
|
||||
ID: channel.ID,
|
||||
Name: "new name",
|
||||
Metadata: "new meta",
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
channel bootstrap.Channel
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update an existing channel",
|
||||
channel: ch,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update a non-existing channel",
|
||||
channel: bootstrap.Channel{ID: ""},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateChannelHandler(tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannelHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
_, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "remove an existing channel",
|
||||
id: channel.ID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove a non-existing channel",
|
||||
id: "unknown",
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.RemoveChannelHandler(tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveCoinfigHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "remove an existing conifg",
|
||||
id: saved.MFThing,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove a non-existing channel",
|
||||
id: "unknown",
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.RemoveConfigHandler(tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnectThingsHandler(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
server := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, server.URL)
|
||||
|
||||
saved, err := svc.Add(validToken, config)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
thingID string
|
||||
channelID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "disconnect",
|
||||
channelID: channel.ID,
|
||||
thingID: saved.MFThing,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect dicsonnected",
|
||||
channelID: channel.ID,
|
||||
thingID: saved.MFThing,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.DisconnectThingHandler(tc.channelID, tc.thingID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package bootstrap
|
||||
|
||||
import "strconv"
|
||||
|
||||
const (
|
||||
// Inactive Thing is created, but not able to exchange messages using Mainflux.
|
||||
Inactive State = iota
|
||||
// Active Thing is created, configured, and whitelisted.
|
||||
Active
|
||||
)
|
||||
|
||||
// State represents corresponding Mainflux Thing state. The possible Config States
|
||||
// as well as description of what that State represents are given in the table:
|
||||
// | State | What it means |
|
||||
// |----------+--------------------------------------------------------------------------------|
|
||||
// | Inactive | Thing is created, but isn't able to communicate over Mainflux |
|
||||
// | Active | Thing is able to communicate using Mainflux |
|
||||
type State int
|
||||
|
||||
// String returns string representation of State.
|
||||
func (s State) String() string {
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: Mainflux Bootstrap service
|
||||
description: HTTP API for managing platform devices and applications configuration.
|
||||
version: "1.0.0"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
paths:
|
||||
/things/configs:
|
||||
post:
|
||||
summary: Adds new config
|
||||
description: |
|
||||
Adds new config to the list of config owned by user identified using
|
||||
the provided access token.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- name: config
|
||||
description: JSON-formatted document describing the new config.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigReq"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: Config registered.
|
||||
headers:
|
||||
Location:
|
||||
type: string
|
||||
description: Created config's relative URL (i.e. /things/configs/{configId}).
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
get:
|
||||
summary: Retrieves managed configs
|
||||
description: |
|
||||
Retrieves a list of managed configs. Due to performance concerns, data
|
||||
is retrieved in subsets. The API configs must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Limit"
|
||||
- $ref: "#/parameters/Offset"
|
||||
- $ref: "#/parameters/State"
|
||||
- $ref: "#/parameters/Name"
|
||||
responses:
|
||||
200:
|
||||
description: |
|
||||
Data retrieved. Configs from this list don't contain channels.
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigList"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/bootstrap/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and external key.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/ConfigAuthorization"
|
||||
- $ref: "#/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/BootstrapRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config. Thing which attempted
|
||||
to bootstrap is saved as an unknown Thing and can be listed and
|
||||
added to the service later.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/configs/{configId}:
|
||||
get:
|
||||
summary: Retrieves config info (with channels)
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigRes"
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
put:
|
||||
summary: Updates config info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Note that the owner, ID, external ID,
|
||||
external key, Mainflux Thing ID and key cannot be changed.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: config
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigUpdateReq"
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Config updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
summary: Removes a Config
|
||||
description: |
|
||||
Removes a Config. In case of successfull removal the service will ensure
|
||||
that the removed config is disconnected from all of the Maifnlux channels.
|
||||
tags:
|
||||
- confgis
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
responses:
|
||||
204:
|
||||
description: Config removed.
|
||||
400:
|
||||
description: Failed due to malformed config's ID.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/configs/connections/{configId}:
|
||||
put:
|
||||
summary: Updates channels the thing is connected to
|
||||
description: |
|
||||
Update connections performs update of the channel list corresponding
|
||||
Thing is connected to.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: channels
|
||||
description: Array if IDs the thing is be connected to.
|
||||
in: body
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Config updated.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/state/{configId}:
|
||||
put:
|
||||
summary: Updates Config state.
|
||||
description: |
|
||||
Updating state represents enabling/disabling Config, i.e. connecting
|
||||
and disconnecting corresponding Mainflux Thing to the list of Channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: state
|
||||
description: New state of the Config.
|
||||
in: body
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
state:
|
||||
type: integer
|
||||
enum:
|
||||
- inactive
|
||||
- active
|
||||
responses:
|
||||
204:
|
||||
description: Config removed.
|
||||
400:
|
||||
description: Failed due to malformed config's ID.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/unknown/configs:
|
||||
get:
|
||||
summary: Get a list of unsucessfully bootstrapped Things
|
||||
description: |
|
||||
Retrieves a list of unknown configs. Due to performance concerns, data
|
||||
is retrieved in subsets. The API configs must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Limit"
|
||||
- $ref: "#/parameters/Offset"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigList"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
ConfigAuthorization:
|
||||
name: configAuthorization
|
||||
description: Configuration external key.
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
ConfigId:
|
||||
name: configId
|
||||
description: Unique Config identifier.
|
||||
in: path
|
||||
type: integer
|
||||
minimum: 1
|
||||
required: true
|
||||
ExternalId:
|
||||
name: externalId
|
||||
description: Unique Config identifier provided by external entity.
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
State:
|
||||
name: state
|
||||
description: A state of items
|
||||
in: query
|
||||
type: integer
|
||||
enum:
|
||||
- inactive
|
||||
- active
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Name of the config. Search by name is partial-match and case-insensitive.
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occured.
|
||||
|
||||
definitions:
|
||||
ConfigList:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results.
|
||||
minimum: 0
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
minimum: 0
|
||||
default: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve.
|
||||
maximum: 100
|
||||
default: 10
|
||||
configs:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/definitions/ConfigRes"
|
||||
required:
|
||||
- confgis
|
||||
State:
|
||||
type: integer
|
||||
enum:
|
||||
- active
|
||||
- inactive
|
||||
ConfigRes:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_thing:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
mainflux_key:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing key.
|
||||
mainflux_channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: ID of the Channel.
|
||||
name:
|
||||
type: string
|
||||
description: Name of the Channel.
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata related to the Channel.
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some uinque identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration
|
||||
state:
|
||||
$ref: '#/definitions/State'
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
BootstrapRes:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_thing:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
mainflux_key:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing key.
|
||||
mainflux_channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration
|
||||
required:
|
||||
- mainflux_thing
|
||||
- mainflux_key
|
||||
- mainflux_channels
|
||||
- content
|
||||
ConfigReq:
|
||||
type: object
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some uinque identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
thing_id:
|
||||
type: string
|
||||
description: ID of the corresponding Mainflux Thing.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigUpdateReq:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- name
|
||||
+43
-22
@@ -7,12 +7,12 @@ make cli
|
||||
|
||||
## Usage
|
||||
### Service
|
||||
#### Get the service verison
|
||||
#### Get the version of Mainflux services
|
||||
```
|
||||
mainflux-cli version
|
||||
```
|
||||
|
||||
### User management
|
||||
### Users management
|
||||
#### Create User
|
||||
```
|
||||
mainflux-cli users create john.doe@email.com password
|
||||
@@ -24,24 +24,19 @@ mainflux-cli users token john.doe@email.com password
|
||||
```
|
||||
|
||||
### System Provisioning
|
||||
#### Provision Device
|
||||
#### Create Thing (type Device)
|
||||
```
|
||||
mainflux-cli things create '{"type":"device", "name":"nyDevice"}' <user_auth_token>
|
||||
mainflux-cli things create '{"type":"device", "name":"myDevice"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Provision Application
|
||||
#### Create Thing (type Application)
|
||||
```
|
||||
mainflux-cli things create '{"type":"app", "name":"nyDevice"}' <user_auth_token>
|
||||
mainflux-cli things create '{"type":"app", "name":"myDevice"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve All Things
|
||||
#### Update Thing
|
||||
```
|
||||
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>
|
||||
mainflux-cli things update '{"id":"<thing_id>", "name":"myNewName"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Remove Thing
|
||||
@@ -49,39 +44,65 @@ mainflux-cli things get <thing_id> --offset=1 --limit=5 <user_auth_token>
|
||||
mainflux-cli things delete <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Provision Channel
|
||||
#### Retrieve a subset list of provisioned Things
|
||||
```
|
||||
mainflux-cli channels create '{"name":"nyChannel"}' <user_auth_token>
|
||||
mainflux-cli things get all --offset=1 --limit=5 <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve All Channels
|
||||
#### Retrieve Thing By ID
|
||||
```
|
||||
mainflux-cli channels get all --offset=1 --limit=5 <user_auth_token>
|
||||
mainflux-cli things get <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrievie Channel By ID
|
||||
#### Create Channel
|
||||
```
|
||||
mainflux-cli channels get <channel_id> --offset=1 --limit=5 <user_auth_token>
|
||||
mainflux-cli channels create '{"name":"myChannel"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Update Channel
|
||||
```
|
||||
mainflux-cli channels update '{"id":"<channel_id>","name":"myNewName"}' <user_auth_token>
|
||||
|
||||
```
|
||||
#### Remove Channel
|
||||
```
|
||||
mainflux-cli channels delete <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of provisioned Channels
|
||||
```
|
||||
mainflux-cli channels get all --offset=1 --limit=5 <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve Channel By ID
|
||||
```
|
||||
mainflux-cli channels get <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
### Access control
|
||||
#### Connect Thing to a Channel
|
||||
#### Connect Thing to Channel
|
||||
```
|
||||
mainflux-cli things connect <thing_id> <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Disconnect Things from a Channel
|
||||
#### Disconnect Thing from Channel
|
||||
```
|
||||
mainflux-cli things disconnect <thing_id> <channel_id> <user_auth_token>
|
||||
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of Channels connected to Thing
|
||||
```
|
||||
mainflux-cli things connections <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of Things connected to Channel
|
||||
```
|
||||
mainflux-cli channels connections <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
### Messaging
|
||||
#### Send a message over HTTP
|
||||
```
|
||||
mainflux-cli msg send <channel_id> '[{"bn":"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>
|
||||
mainflux-cli msg send <channel_id> '[{"bn":"Dev1","n":"temp","v":20}, {"n":"hum","v":40}, {"bn":"Dev2", "n":"temp","v":20}, {"n":"hum","v":40}]' <thing_auth_token>
|
||||
```
|
||||
|
||||
+83
-58
@@ -8,16 +8,12 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const channelsEP = "channels"
|
||||
|
||||
var cmdChannels = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "create",
|
||||
@@ -25,38 +21,77 @@ var cmdChannels = []cobra.Command{
|
||||
Long: `Creates new channel and generates it's UUID`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
CreateChannel(args[0], args[1])
|
||||
|
||||
var channel mfxsdk.Channel
|
||||
if err := json.Unmarshal([]byte(args[0]), &channel); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := sdk.CreateChannel(channel, args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logCreated(id)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "get",
|
||||
Short: "get all/<channel_id> <user_auth_token>",
|
||||
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)
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if args[0] == "all" {
|
||||
GetChannels(args[1])
|
||||
l, err := sdk.Channels(args[1], uint64(Offset), uint64(Limit))
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(l)
|
||||
return
|
||||
}
|
||||
GetChannel(args[0], args[1])
|
||||
|
||||
c, err := sdk.Channel(args[0], args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(c)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update <channel_id> <JSON_string> <user_auth_token>",
|
||||
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)
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
UpdateChannel(args[0], args[1], args[2])
|
||||
|
||||
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{
|
||||
@@ -65,63 +100,53 @@ var cmdChannels = []cobra.Command{
|
||||
Long: `Delete channel by ID`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
DeleteChannel(args[0], args[1])
|
||||
|
||||
if err := sdk.DeleteChannel(args[0], args[1]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "connections",
|
||||
Short: "connections <channel_id> <user_auth_token>",
|
||||
Long: `List of Things connected to Channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
cl, err := sdk.ThingsByChannel(args[1], args[0], uint64(Offset), uint64(Limit))
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(cl)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewChannelsCmd returns channels command.
|
||||
func NewChannelsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "channels",
|
||||
Short: "Manipulation with channels",
|
||||
Long: `Manipulation with channels: create, delete or update channels`,
|
||||
Short: "Channels management",
|
||||
Long: `Channels management: create, get, update or delete Channel and get list of Things connected to Channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage("channels [create | get | update | delete | connections]")
|
||||
},
|
||||
}
|
||||
|
||||
for i, _ := range cmdChannels {
|
||||
for i := range cmdChannels {
|
||||
cmd.AddCommand(&cmdChannels[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
|
||||
// CreateChannel - creates new channel and generates UUID
|
||||
func CreateChannel(data, token string) {
|
||||
url := fmt.Sprintf("%s/%s", serverAddr, channelsEP)
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(data))
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
// GetChannels - gets all channels
|
||||
func GetChannels(token string) {
|
||||
url := fmt.Sprintf("%s/%s?offset=%s&limit=%s",
|
||||
serverAddr, channelsEP, strconv.Itoa(Offset), strconv.Itoa(Limit))
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
// GetChannel - gets channel by ID
|
||||
func GetChannel(id, token string) {
|
||||
url := fmt.Sprintf("%s/%s/%s", serverAddr, channelsEP, id)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
// UpdateChannel - update a channel
|
||||
func UpdateChannel(id, data, token string) {
|
||||
url := fmt.Sprintf("%s/%s/%s", serverAddr, channelsEP, id)
|
||||
req, err := http.NewRequest("PUT", url, strings.NewReader(data))
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
// DeleteChannel - removes channel
|
||||
func DeleteChannel(id, token string) {
|
||||
url := fmt.Sprintf("%s/%s/%s", serverAddr, channelsEP, id)
|
||||
req, err := http.NewRequest("DELETE", url, nil)
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
const (
|
||||
defCertsPath = "/src/github.com/mainflux/mainflux/docker/ssl/certs/"
|
||||
envCertFile = "MF_CERT_FILE"
|
||||
envKeyFile = "MF_KEY_FILE"
|
||||
envCaFile = "MF_CA_FILE"
|
||||
)
|
||||
|
||||
var (
|
||||
httpClient = &http.Client{}
|
||||
serverAddr = fmt.Sprintf("https://%s", "localhost")
|
||||
|
||||
defCertFile = fmt.Sprintf("%s%s%s", os.Getenv("GOPATH"), defCertsPath, "mainflux-server.crt")
|
||||
defKeyFile = fmt.Sprintf("%s%s%s", os.Getenv("GOPATH"), defCertsPath, "mainflux-server.key")
|
||||
defCaFile = fmt.Sprintf("%s%s%s", os.Getenv("GOPATH"), defCertsPath, "ca.crt")
|
||||
)
|
||||
|
||||
// SetServerAddr - set addr using host and port
|
||||
func SetServerAddr(proto string, host string, port int) {
|
||||
serverAddr = fmt.Sprintf("%s://%s", proto, host)
|
||||
|
||||
if port != 0 {
|
||||
serverAddr = fmt.Sprintf("%s:%s", serverAddr, strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
|
||||
func SetCerts() {
|
||||
// Set certificates paths
|
||||
certFile := mainflux.Env(envCertFile, defCertFile)
|
||||
keyFile := mainflux.Env(envKeyFile, defKeyFile)
|
||||
caFile := mainflux.Env(envCaFile, defCaFile)
|
||||
|
||||
// Load client cert
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Load CA cert
|
||||
caCert, err := ioutil.ReadFile(caFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
// Setup HTTPS client
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: caCertPool,
|
||||
}
|
||||
tlsConfig.BuildNameToCertificate()
|
||||
transport := &http.Transport{TLSClientConfig: tlsConfig}
|
||||
httpClient = &http.Client{Transport: transport}
|
||||
}
|
||||
+37
-26
@@ -7,53 +7,64 @@
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
const contentTypeSenml = "application/senml+json"
|
||||
|
||||
var cmdMessages = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "send",
|
||||
Short: "send <channel_id> <JSON_string> <client_token>",
|
||||
Short: "send <channel_id>[.<subtopic>...] <JSON_string> <thing_key>",
|
||||
Long: `Sends message on the channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
SendMsg(args[0], args[1], args[2])
|
||||
|
||||
if err := sdk.SendMessage(args[0], args[1], args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "read",
|
||||
Short: "read <channel_id>[.<subtopic>...] <thing_key>",
|
||||
Long: `Reads all channel messages`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
m, err := sdk.ReadMessages(args[0], args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(m)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewMessagesCmd returns messages command.
|
||||
func NewMessagesCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "msg",
|
||||
Short: "Send or retrieve messages",
|
||||
Long: `Send or retrieve messages: control message flow on the channel`,
|
||||
Use: "messages",
|
||||
Short: "Send or read messages",
|
||||
Long: `Send or read messages using the http-adapter and the configured database reader`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("messages [send | read]")
|
||||
},
|
||||
}
|
||||
|
||||
for i, _ := range cmdMessages {
|
||||
for i := range cmdMessages {
|
||||
cmd.AddCommand(&cmdMessages[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
|
||||
// SendMsg - publishes SenML message on the channel
|
||||
func SendMsg(id, msg, token string) {
|
||||
url := serverAddr + "/http/channels/" + id + "/messages"
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(msg))
|
||||
LogError(err)
|
||||
|
||||
req.Header.Set("Authorization", token)
|
||||
req.Header.Add("Content-Type", contentTypeSenml)
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
FormatResLog(resp, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/docker/docker/pkg/namesgenerator"
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var errMalformedCSV = errors.New("malformed CSV")
|
||||
|
||||
func createThing(name, kind, token string) (mfxsdk.Thing, error) {
|
||||
id, err := sdk.CreateThing(mfxsdk.Thing{Name: name, Type: kind}, token)
|
||||
if err != nil {
|
||||
return mfxsdk.Thing{}, err
|
||||
}
|
||||
|
||||
t, err := sdk.Thing(id, token)
|
||||
if err != nil {
|
||||
return mfxsdk.Thing{}, err
|
||||
}
|
||||
|
||||
m := mfxsdk.Thing{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Type: kind,
|
||||
Key: t.Key,
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func createChannel(name, token string) (mfxsdk.Channel, error) {
|
||||
id, err := sdk.CreateChannel(mfxsdk.Channel{Name: name}, token)
|
||||
if err != nil {
|
||||
return mfxsdk.Channel{}, nil
|
||||
}
|
||||
|
||||
c := mfxsdk.Channel{
|
||||
ID: id,
|
||||
Name: name,
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var cmdProvision = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "things",
|
||||
Short: "things <things_csv> <user_token>",
|
||||
Long: `Provisions things`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
things := []mfxsdk.Thing{}
|
||||
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := os.Open(args[0])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
reader := csv.NewReader(bufio.NewReader(c))
|
||||
|
||||
for {
|
||||
l, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(l) < 2 {
|
||||
logError(errMalformedCSV)
|
||||
return
|
||||
}
|
||||
|
||||
m, err := createThing(l[0], l[1], args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
things = append(things, m)
|
||||
}
|
||||
|
||||
logJSON(things)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "channels",
|
||||
Short: "channels <channels_csv> <user_token>",
|
||||
Long: `Provisions channels`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
channels := []mfxsdk.Channel{}
|
||||
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := os.Open(args[0])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
reader := csv.NewReader(bufio.NewReader(c))
|
||||
|
||||
for {
|
||||
l, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(l) < 1 {
|
||||
logError(errMalformedCSV)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := createChannel(l[0], args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
channels = append(channels, c)
|
||||
}
|
||||
|
||||
logJSON(channels)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "test",
|
||||
Short: "test",
|
||||
Long: `Provisions test setup: one test user, two things and two channels. \
|
||||
Connect both things to one of the channels, \
|
||||
and only on thing to other channel.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
numThings := 2
|
||||
numChan := 2
|
||||
things := []mfxsdk.Thing{}
|
||||
channels := []mfxsdk.Channel{}
|
||||
|
||||
if len(args) != 0 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
un := fmt.Sprintf("%s@email.com", namesgenerator.GetRandomName(0))
|
||||
// Create test user
|
||||
user := mfxsdk.User{
|
||||
Email: un,
|
||||
Password: "123",
|
||||
}
|
||||
if err := sdk.CreateUser(user); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ut, err := sdk.CreateToken(user)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create things
|
||||
for i := 0; i < numThings; i++ {
|
||||
n := fmt.Sprintf("d%d", i)
|
||||
k := "device"
|
||||
if i%2 != 0 {
|
||||
k = "app"
|
||||
}
|
||||
|
||||
m, err := createThing(n, k, ut)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
things = append(things, m)
|
||||
}
|
||||
// Create channels
|
||||
for i := 0; i < numChan; i++ {
|
||||
n := fmt.Sprintf("c%d", i)
|
||||
c, err := createChannel(n, ut)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
channels = append(channels, c)
|
||||
}
|
||||
|
||||
// Connect things to channels - first thing to both channels, second only to first
|
||||
for i := 0; i < numThings; i++ {
|
||||
if err := sdk.ConnectThing(things[i].ID, channels[i].ID, ut); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if i%2 == 0 {
|
||||
if i+1 >= len(channels) {
|
||||
break
|
||||
}
|
||||
if err := sdk.ConnectThing(things[i].ID, channels[i+1].ID, ut); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logJSON(user, ut, things, channels)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewProvisionCmd returns provision command.
|
||||
func NewProvisionCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "provision",
|
||||
Short: "Provision things and channels from config file",
|
||||
Long: `Provision things and channels: use csv config file to provision things and channels`,
|
||||
}
|
||||
|
||||
for i := range cmdProvision {
|
||||
cmd.AddCommand(&cmdProvision[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
+18
@@ -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
|
||||
}
|
||||
+99
-77
@@ -8,11 +8,9 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -25,26 +23,52 @@ var cmdThings = []cobra.Command{
|
||||
Long: `Create new thing, generate his UUID and store it`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
CreateThing(args[0], args[1])
|
||||
|
||||
var thing mfxsdk.Thing
|
||||
if err := json.Unmarshal([]byte(args[0]), &thing); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := sdk.CreateThing(thing, args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logCreated(id)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "get",
|
||||
Short: "get all/<thing_id> <user_auth_token>",
|
||||
Long: `Get all thingss or thing by id`,
|
||||
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)
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if args[0] == "all" {
|
||||
GetThings(args[1])
|
||||
l, err := sdk.Things(args[1], uint64(Offset), uint64(Limit))
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(l)
|
||||
return
|
||||
}
|
||||
GetThing(args[0], args[1])
|
||||
|
||||
t, err := sdk.Thing(args[0], args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(t)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
@@ -53,22 +77,40 @@ var cmdThings = []cobra.Command{
|
||||
Long: `Removes thing from database`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
DeleteThing(args[0], args[1])
|
||||
|
||||
if err := sdk.DeleteThing(args[0], args[1]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update <thing_id> <JSON_string> <user_auth_token>",
|
||||
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)
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
UpdateThing(args[0], args[1], args[2])
|
||||
|
||||
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{
|
||||
@@ -77,10 +119,16 @@ var cmdThings = []cobra.Command{
|
||||
Long: `Connect thing to the channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
ConnectThing(args[0], args[1], args[2])
|
||||
|
||||
if err := sdk.ConnectThing(args[0], args[1], args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
@@ -89,79 +137,53 @@ var cmdThings = []cobra.Command{
|
||||
Long: `Disconnect thing to the channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
DisconnectThing(args[0], args[1], args[2])
|
||||
|
||||
if err := sdk.DisconnectThing(args[0], args[1], args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "connections",
|
||||
Short: "connections <thing_id> <user_auth_token>",
|
||||
Long: `List of Channels connected to Thing`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
cl, err := sdk.ChannelsByThing(args[1], args[0], uint64(Offset), uint64(Limit))
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(cl)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewThingsCmd returns things command.
|
||||
func NewThingsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "things",
|
||||
Short: "things <options>",
|
||||
Long: `Things handling: create, delete or update things.`,
|
||||
Short: "Things management",
|
||||
Long: `Things management: create, get, update or delete Thing, connect or disconnect Thing from Channel and get the list of Channels connected to Thing`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage("things [create | get | update | delete | connect | disconnect | connections]")
|
||||
},
|
||||
}
|
||||
|
||||
for i, _ := range cmdThings {
|
||||
for i := range cmdThings {
|
||||
cmd.AddCommand(&cmdThings[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
|
||||
// CreateThing - creates new thing and generates thing UUID
|
||||
func CreateThing(data, token string) {
|
||||
url := fmt.Sprintf("%s/%s", serverAddr, thingsEP)
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(data))
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
// GetThings - gets all things
|
||||
func GetThings(token string) {
|
||||
url := fmt.Sprintf("%s/%s?offset=%s&limit=%s",
|
||||
serverAddr, thingsEP, strconv.Itoa(Offset), strconv.Itoa(Limit))
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
// GetThing - gets thing by ID
|
||||
func GetThing(id, token string) {
|
||||
url := fmt.Sprintf("%s/%s/%s", serverAddr, thingsEP, id)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
// UpdateThing - updates thing by ID
|
||||
func UpdateThing(id, data, token string) {
|
||||
url := fmt.Sprintf("%s/%s/%s", serverAddr, thingsEP, id)
|
||||
req, err := http.NewRequest("PUT", url, strings.NewReader(data))
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
// DeleteThing - removes thing
|
||||
func DeleteThing(id, token string) {
|
||||
url := fmt.Sprintf("%s/%s/%s", serverAddr, thingsEP, id)
|
||||
req, err := http.NewRequest("DELETE", url, nil)
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
// ConnectThing - connect thing to a channel
|
||||
func ConnectThing(cliId, chanId, token string) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/%s", serverAddr, channelsEP,
|
||||
chanId, thingsEP, cliId)
|
||||
req, err := http.NewRequest("PUT", url, nil)
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
// DisconnectThing - connect thing to a channel
|
||||
func DisconnectThing(cliId, chanId, token string) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/%s", serverAddr, channelsEP,
|
||||
chanId, thingsEP, cliId)
|
||||
req, err := http.NewRequest("DELETE", url, nil)
|
||||
SendRequest(req, token, err)
|
||||
}
|
||||
|
||||
+31
-27
@@ -8,9 +8,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -21,10 +19,20 @@ var cmdUsers = []cobra.Command{
|
||||
Long: `Creates new user`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
CreateUser(args[0], args[1])
|
||||
|
||||
user := mfxsdk.User{
|
||||
Email: args[0],
|
||||
Password: args[1],
|
||||
}
|
||||
if err := sdk.CreateUser(user); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
@@ -33,43 +41,39 @@ var cmdUsers = []cobra.Command{
|
||||
Long: `Creates new token`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
CreateToken(args[0], args[1])
|
||||
|
||||
user := mfxsdk.User{
|
||||
Email: args[0],
|
||||
Password: args[1],
|
||||
}
|
||||
token, err := sdk.CreateToken(user)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logCreated(token)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewUsersCmd returns users command.
|
||||
func NewUsersCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "users",
|
||||
Short: "users create/token <email> <password>",
|
||||
Long: `Manages users in the system (create account or token)`,
|
||||
Short: "Users management",
|
||||
Long: `Users management: create accounts and tokens"`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
LogUsage(cmd.Short)
|
||||
logUsage("Usage: users [create | token]")
|
||||
},
|
||||
}
|
||||
|
||||
for i, _ := range cmdUsers {
|
||||
for i := range cmdUsers {
|
||||
cmd.AddCommand(&cmdUsers[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
|
||||
// CreateUser - create user
|
||||
func CreateUser(user, pwd string) {
|
||||
msg := fmt.Sprintf(`{"email": "%s", "password": "%s"}`, user, pwd)
|
||||
url := fmt.Sprintf("%s/users", serverAddr)
|
||||
resp, err := httpClient.Post(url, contentType, strings.NewReader(msg))
|
||||
FormatResLog(resp, err)
|
||||
}
|
||||
|
||||
// CreateToken - create user token
|
||||
func CreateToken(user, pwd string) {
|
||||
msg := fmt.Sprintf(`{"email": "%s", "password": "%s"}`, user, pwd)
|
||||
url := fmt.Sprintf("%s/tokens", serverAddr)
|
||||
resp, err := httpClient.Post(url, contentType, strings.NewReader(msg))
|
||||
FormatResLog(resp, err)
|
||||
}
|
||||
|
||||
+32
-50
@@ -8,68 +8,50 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/hokaccha/go-prettyjson"
|
||||
prettyjson "github.com/hokaccha/go-prettyjson"
|
||||
)
|
||||
|
||||
const contentType = "application/json"
|
||||
var (
|
||||
// Limit query parameter
|
||||
Limit uint = 10
|
||||
// Offset query parameter
|
||||
Offset uint
|
||||
)
|
||||
|
||||
var Limit = 10
|
||||
var Offset = 0
|
||||
|
||||
func SendRequest(req *http.Request, token string, e error) {
|
||||
req.Header.Set("Authorization", token)
|
||||
req.Header.Add("Content-Type", contentType)
|
||||
if e != nil {
|
||||
LogError(e)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
FormatResLog(resp, err)
|
||||
}
|
||||
|
||||
// FormatResLog - format http response
|
||||
func FormatResLog(resp *http.Response, err error) {
|
||||
if err != nil {
|
||||
LogError(err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
fmt.Printf(color.CyanString("%s %s\nContent-Length: %v\n\n"),
|
||||
resp.Proto, resp.Status, resp.ContentLength)
|
||||
|
||||
if len(resp.Header.Get("Location")) != 0 {
|
||||
fmt.Printf(color.BlueString("Resource location: %s\n\n"),
|
||||
resp.Header.Get("Location"))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
LogError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(body) != 0 {
|
||||
pj, err := prettyjson.Format([]byte(body))
|
||||
func logJSON(iList ...interface{}) {
|
||||
for _, i := range iList {
|
||||
m, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n\n", color.BlueString(string(body)))
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s\n\n", string(pj))
|
||||
|
||||
pj, err := prettyjson.Format(m)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s\n\n", string(pj))
|
||||
}
|
||||
}
|
||||
|
||||
func LogUsage(u string) {
|
||||
fmt.Printf(color.YellowString("Usage: %s\n\n"), u)
|
||||
func logUsage(u string) {
|
||||
fmt.Printf(color.YellowString("\nusage: %s\n\n"), u)
|
||||
}
|
||||
|
||||
func LogError(err error) {
|
||||
fmt.Printf("%s\n\n", color.RedString(err.Error()))
|
||||
func logError(err error) {
|
||||
fmt.Printf("\n%s\n\n", color.RedString(err.Error()))
|
||||
}
|
||||
|
||||
func logOK() {
|
||||
fmt.Printf("\n%s\n\n", color.BlueString("ok"))
|
||||
}
|
||||
|
||||
func logCreated(e string) {
|
||||
fmt.Printf(color.BlueString("\ncreated: %s\n\n"), e)
|
||||
}
|
||||
|
||||
+11
-14
@@ -7,25 +7,22 @@
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
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 checkt.`,
|
||||
Short: "Mainflux services version",
|
||||
Long: `Mainflux services version: get version of Mainflux Things Service`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Version()
|
||||
v, err := sdk.Version()
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(v)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Version - server health check
|
||||
func Version() {
|
||||
url := fmt.Sprintf("%s/version", serverAddr)
|
||||
FormatResLog(httpClient.Get(url))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
r "github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
api "github.com/mainflux/mainflux/bootstrap/api"
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
"github.com/mainflux/mainflux/bootstrap/redis"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
usersapi "github.com/mainflux/mainflux/users/api/grpc"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
defLogLevel = "error"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "bootstrap"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defPort = "8180"
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defBaseURL = "http://localhost"
|
||||
defThingsPrefix = ""
|
||||
defUsersURL = "localhost:8181"
|
||||
defESURL = "localhost:6379"
|
||||
defESPass = ""
|
||||
defESDB = "0"
|
||||
defInstanceName = "bootstrap"
|
||||
|
||||
envLogLevel = "MF_BOOTSTRAP_LOG_LEVEL"
|
||||
envDBHost = "MF_BOOTSTRAP_DB_HOST"
|
||||
envDBPort = "MF_BOOTSTRAP_DB_PORT"
|
||||
envDBUser = "MF_BOOTSTRAP_DB_USER"
|
||||
envDBPass = "MF_BOOTSTRAP_DB_PASS"
|
||||
envDBName = "MF_BOOTSTRAP_DB"
|
||||
envDBSSLMode = "MF_BOOTSTRAP_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_BOOTSTRAP_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_BOOTSTRAP_DB_SSL_KEY"
|
||||
envDBSSLRootCert = "MF_BOOTSTRAP_DB_SSL_ROOT_CERT"
|
||||
envClientTLS = "MF_BOOTSTRAP_CLIENT_TLS"
|
||||
envCACerts = "MF_BOOTSTRAP_CA_CERTS"
|
||||
envPort = "MF_BOOTSTRAP_PORT"
|
||||
envServerCert = "MF_BOOTSTRAP_SERVER_CERT"
|
||||
envServerKey = "MF_BOOTSTRAP_SERVER_KEY"
|
||||
envBaseURL = "MF_SDK_BASE_URL"
|
||||
envThingsPrefix = "MF_SDK_THINGS_PREFIX"
|
||||
envUsersURL = "MF_USERS_URL"
|
||||
envESURL = "MF_THINGS_ES_URL"
|
||||
envESPass = "MF_THINGS_ES_PASS"
|
||||
envESDB = "MF_THINGS_ES_DB"
|
||||
envInstanceName = "MF_BOOTSTRAP_INSTANCE_NAME"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
logLevel string
|
||||
dbConfig postgres.Config
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
httpPort string
|
||||
serverCert string
|
||||
serverKey string
|
||||
baseURL string
|
||||
thingsPrefix string
|
||||
usersURL string
|
||||
|
||||
esURL string
|
||||
esPass string
|
||||
esDB string
|
||||
instanceName string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
db := connectToDB(cfg.dbConfig, logger)
|
||||
defer db.Close()
|
||||
|
||||
conn := connectToUsers(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
esConn := connectToRedis(cfg.esURL, cfg.esPass, cfg.esDB, logger)
|
||||
defer esConn.Close()
|
||||
|
||||
svc := newService(conn, db, logger, cfg)
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg, logger, errs)
|
||||
go subscribeToThingsES(svc, esConn, cfg.instanceName, logger)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Bootstrap service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
tls = false
|
||||
}
|
||||
dbConfig := postgres.Config{
|
||||
Host: mainflux.Env(envDBHost, defDBHost),
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Name: mainflux.Env(envDBName, defDBName),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
|
||||
}
|
||||
|
||||
return config{
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
dbConfig: dbConfig,
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
httpPort: mainflux.Env(envPort, defPort),
|
||||
serverCert: mainflux.Env(envServerCert, defServerCert),
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
baseURL: mainflux.Env(envBaseURL, defBaseURL),
|
||||
thingsPrefix: mainflux.Env(envThingsPrefix, defThingsPrefix),
|
||||
usersURL: mainflux.Env(envUsersURL, defUsersURL),
|
||||
|
||||
esURL: mainflux.Env(envESURL, defESURL),
|
||||
esPass: mainflux.Env(envESPass, defESPass),
|
||||
esDB: mainflux.Env(envESDB, defESDB),
|
||||
instanceName: mainflux.Env(envInstanceName, defInstanceName),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToDB(cfg postgres.Config, logger logger.Logger) *sql.DB {
|
||||
db, err := postgres.Connect(cfg)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func connectToRedis(redisURL, redisPass, redisDB string, logger logger.Logger) *r.Client {
|
||||
db, err := strconv.Atoi(redisDB)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to redis: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return r.NewClient(&r.Options{
|
||||
Addr: redisURL,
|
||||
Password: redisPass,
|
||||
DB: db,
|
||||
})
|
||||
}
|
||||
|
||||
func newService(conn *grpc.ClientConn, db *sql.DB, logger logger.Logger, cfg config) bootstrap.Service {
|
||||
thingsRepo := postgres.NewConfigRepository(db, logger)
|
||||
|
||||
config := mfsdk.Config{
|
||||
BaseURL: cfg.baseURL,
|
||||
ThingsPrefix: cfg.thingsPrefix,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
users := usersapi.NewClient(conn)
|
||||
|
||||
svc := bootstrap.New(users, thingsRepo, sdk)
|
||||
svc = api.NewLoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "bootstrap",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "bootstrap",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
return svc
|
||||
}
|
||||
|
||||
func connectToUsers(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
var opts []grpc.DialOption
|
||||
if cfg.clientTLS {
|
||||
if cfg.caCerts != "" {
|
||||
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create tls credentials: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(tpc))
|
||||
}
|
||||
} else {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.usersURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func startHTTPServer(svc bootstrap.Service, cfg config, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.httpPort)
|
||||
if cfg.serverCert != "" || cfg.serverKey != "" {
|
||||
logger.Info(fmt.Sprintf("Bootstrap service started using https on port %s with cert %s key %s",
|
||||
cfg.httpPort, cfg.serverCert, cfg.serverKey))
|
||||
errs <- http.ListenAndServeTLS(p, cfg.serverCert, cfg.serverKey, api.MakeHandler(svc, bootstrap.NewConfigReader()))
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Bootstrap service started using http on port %s", cfg.httpPort))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, bootstrap.NewConfigReader()))
|
||||
}
|
||||
|
||||
func subscribeToThingsES(svc bootstrap.Service, client *r.Client, consumer string, logger logger.Logger) {
|
||||
eventStore := redis.NewEventStore(svc, client, consumer, logger)
|
||||
logger.Info("Subscribed to Redis Event Store")
|
||||
if err := eventStore.Subscribe("mainflux.things"); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Botstrap service failed to subscribe to event sourcing: %s", err))
|
||||
}
|
||||
}
|
||||
@@ -9,54 +9,69 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/readers"
|
||||
"github.com/mainflux/mainflux/readers/api"
|
||||
"github.com/mainflux/mainflux/readers/cassandra"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
sep = ","
|
||||
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defCluster = "127.0.0.1"
|
||||
defKeyspace = "mainflux"
|
||||
defThingsURL = "localhost:8181"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
|
||||
envLogLevel = "MF_CASSANDRA_READER_LOG_LEVEL"
|
||||
envPort = "MF_CASSANDRA_READER_PORT"
|
||||
envCluster = "MF_CASSANDRA_READER_DB_CLUSTER"
|
||||
envKeyspace = "MF_CASSANDRA_READER_DB_KEYSPACE"
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
envClientTLS = "MF_CASSANDRA_READER_CLIENT_TLS"
|
||||
envCACerts = "MF_CASSANDRA_READER_CA_CERTS"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
logLevel string
|
||||
port string
|
||||
cluster string
|
||||
keyspace string
|
||||
thingsURL string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
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)
|
||||
conn := connectToThings(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
tc := thingsapi.NewClient(conn)
|
||||
@@ -72,20 +87,28 @@ func main() {
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err := <-errs
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Cassandra reader service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
return config{
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
cluster: mainflux.Env(envCluster, defCluster),
|
||||
keyspace: mainflux.Env(envKeyspace, defKeyspace),
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToCassandra(cluster, keyspace string, logger log.Logger) *gocql.Session {
|
||||
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))
|
||||
@@ -95,17 +118,31 @@ func connectToCassandra(cluster, keyspace string, logger log.Logger) *gocql.Sess
|
||||
return session
|
||||
}
|
||||
|
||||
func connectToThings(url string, logger log.Logger) *grpc.ClientConn {
|
||||
conn, err := grpc.Dial(url, grpc.WithInsecure())
|
||||
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
var opts []grpc.DialOption
|
||||
if cfg.clientTLS {
|
||||
if cfg.caCerts != "" {
|
||||
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(tpc))
|
||||
}
|
||||
} else {
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func newService(session *gocql.Session, logger log.Logger) readers.MessageRepository {
|
||||
func newService(session *gocql.Session, logger logger.Logger) readers.MessageRepository {
|
||||
repo := cassandra.New(session)
|
||||
repo = api.LoggingMiddleware(repo, logger)
|
||||
repo = api.MetricsMiddleware(
|
||||
@@ -127,7 +164,7 @@ func newService(session *gocql.Session, logger log.Logger) readers.MessageReposi
|
||||
return repo
|
||||
}
|
||||
|
||||
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, port string, errs chan error, logger log.Logger) {
|
||||
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"))
|
||||
|
||||
@@ -9,6 +9,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -18,22 +19,25 @@ import (
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/writers"
|
||||
"github.com/mainflux/mainflux/writers/cassandra"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
"github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
sep = ","
|
||||
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"
|
||||
@@ -41,6 +45,7 @@ const (
|
||||
|
||||
type config struct {
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
cluster string
|
||||
keyspace string
|
||||
@@ -49,7 +54,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())
|
||||
}
|
||||
|
||||
nc := connectToNATS(cfg.natsURL, logger)
|
||||
defer nc.Close()
|
||||
@@ -58,7 +66,7 @@ func main() {
|
||||
defer session.Close()
|
||||
|
||||
repo := newService(session, logger)
|
||||
if err := writers.Start(nc, logger, repo); err != nil {
|
||||
if err := writers.Start(nc, repo, queue, logger); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create Cassandra writer: %s", err))
|
||||
}
|
||||
|
||||
@@ -72,20 +80,21 @@ func main() {
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err := <-errs
|
||||
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 log.Logger) *nats.Conn {
|
||||
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))
|
||||
@@ -95,7 +104,7 @@ func connectToNATS(url string, logger log.Logger) *nats.Conn {
|
||||
return nc
|
||||
}
|
||||
|
||||
func connectToCassandra(cluster, keyspace string, logger log.Logger) *gocql.Session {
|
||||
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))
|
||||
@@ -105,7 +114,7 @@ func connectToCassandra(cluster, keyspace string, logger log.Logger) *gocql.Sess
|
||||
return session
|
||||
}
|
||||
|
||||
func newService(session *gocql.Session, logger log.Logger) writers.MessageRepository {
|
||||
func newService(session *gocql.Session, logger logger.Logger) writers.MessageRepository {
|
||||
repo := cassandra.New(session)
|
||||
repo = writers.LoggingMiddleware(repo, logger)
|
||||
repo = writers.MetricsMiddleware(
|
||||
@@ -127,7 +136,7 @@ func newService(session *gocql.Session, logger log.Logger) writers.MessageReposi
|
||||
return repo
|
||||
}
|
||||
|
||||
func startHTTPServer(port string, errs chan error, logger log.Logger) {
|
||||
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())
|
||||
|
||||
+76
-27
@@ -11,35 +11,30 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/mainflux/mainflux/cli"
|
||||
"github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
conf := struct {
|
||||
host string
|
||||
port int
|
||||
insecure bool
|
||||
}{
|
||||
"localhost",
|
||||
0,
|
||||
false,
|
||||
msgContentType := string(sdk.CTJSONSenML)
|
||||
sdkConf := sdk.Config{
|
||||
BaseURL: "http://localhost",
|
||||
ReaderURL: "http://localhost:8905",
|
||||
ReaderPrefix: "",
|
||||
UsersPrefix: "",
|
||||
ThingsPrefix: "",
|
||||
HTTPAdapterPrefix: "http",
|
||||
MsgContentType: sdk.ContentType(msgContentType),
|
||||
TLSVerification: false,
|
||||
}
|
||||
|
||||
// Root
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "mainflux-cli",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
var proto string
|
||||
|
||||
if conf.insecure {
|
||||
proto = "http"
|
||||
} else {
|
||||
proto = "https"
|
||||
cli.SetCerts()
|
||||
}
|
||||
|
||||
cli.SetServerAddr(proto, conf.host, conf.port)
|
||||
sdkConf.MsgContentType = sdk.ContentType(msgContentType)
|
||||
s := sdk.NewSDK(sdkConf)
|
||||
cli.SetSDK(s)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,6 +44,7 @@ func main() {
|
||||
thingsCmd := cli.NewThingsCmd()
|
||||
channelsCmd := cli.NewChannelsCmd()
|
||||
messagesCmd := cli.NewMessagesCmd()
|
||||
provisionCmd := cli.NewProvisionCmd()
|
||||
|
||||
// Root Commands
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
@@ -56,20 +52,73 @@ func main() {
|
||||
rootCmd.AddCommand(thingsCmd)
|
||||
rootCmd.AddCommand(channelsCmd)
|
||||
rootCmd.AddCommand(messagesCmd)
|
||||
rootCmd.AddCommand(provisionCmd)
|
||||
|
||||
// Root Flags
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&conf.host, "host", "m", conf.host, "HTTP Host address")
|
||||
rootCmd.PersistentFlags().IntVarP(
|
||||
&conf.port, "port", "p", conf.port, "HTTP Host Port")
|
||||
&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(
|
||||
&conf.insecure, "insecure", "i", false, "do not use TLS")
|
||||
&sdkConf.TLSVerification,
|
||||
"insecure",
|
||||
"i",
|
||||
sdkConf.TLSVerification,
|
||||
"Do not check for TLS cert",
|
||||
)
|
||||
|
||||
// Client and Channels Flags
|
||||
rootCmd.PersistentFlags().IntVarP(
|
||||
&cli.Limit, "limit", "l", 100, "limit query parameter")
|
||||
rootCmd.PersistentFlags().IntVarP(
|
||||
&cli.Offset, "offset", "o", 0, "offset query parameter")
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
gocoap "github.com/dustin/go-coap"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/api"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
logger "github.com/mainflux/mainflux/logger"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
defPort = "5683"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defThingsURL = "localhost:8181"
|
||||
defLogLevel = "error"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defPingPeriod = "12"
|
||||
|
||||
envPort = "MF_COAP_ADAPTER_PORT"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
envLogLevel = "MF_COAP_ADAPTER_LOG_LEVEL"
|
||||
envClientTLS = "MF_COAP_ADAPTER_CLIENT_TLS"
|
||||
envCACerts = "MF_COAP_ADAPTER_CA_CERTS"
|
||||
envPingPeriod = "MF_COAP_ADAPTER_PING_PERIOD"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
port string
|
||||
natsURL string
|
||||
thingsURL string
|
||||
logLevel string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
pingPeriod time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
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 {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn := connectToThings(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
cc := thingsapi.NewClient(conn)
|
||||
respChan := make(chan string, 10000)
|
||||
pubsub := nats.New(nc)
|
||||
svc := coap.New(pubsub, respChan)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "coap_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "coap_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(cfg.port, logger, errs)
|
||||
go startCOAPServer(cfg, svc, cc, respChan, 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("CoAP adapter terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
pp, err := strconv.ParseInt(mainflux.Env(envPingPeriod, defPingPeriod), 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envPingPeriod)
|
||||
}
|
||||
|
||||
if pp < 1 || pp > 24 {
|
||||
log.Fatalf("Value of %s must be between 1 and 24", envPingPeriod)
|
||||
}
|
||||
|
||||
return config{
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
pingPeriod: time.Duration(pp),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
var opts []grpc.DialOption
|
||||
if cfg.clientTLS {
|
||||
if cfg.caCerts != "" {
|
||||
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(tpc))
|
||||
}
|
||||
} else {
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
func startHTTPServer(port string, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("CoAP service started, exposed port %s", port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHTTPHandler())
|
||||
}
|
||||
|
||||
func startCOAPServer(cfg config, svc coap.Service, auth mainflux.ThingsServiceClient, respChan chan<- string, l logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.port)
|
||||
l.Info(fmt.Sprintf("CoAP adapter service started, exposed port %s", cfg.port))
|
||||
errs <- gocoap.ListenAndServe("udp", p, api.MakeCOAPHandler(svc, auth, l, respChan, cfg.pingPeriod))
|
||||
}
|
||||
+73
-24
@@ -9,17 +9,21 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
adapter "github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/http/api"
|
||||
"github.com/mainflux/mainflux/http/nats"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"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"
|
||||
@@ -27,41 +31,46 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defPort string = "8180"
|
||||
defNatsURL string = broker.DefaultURL
|
||||
defThingsURL string = "localhost:8181"
|
||||
envPort string = "MF_HTTP_ADAPTER_PORT"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envThingsURL string = "MF_THINGS_URL"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defPort = "8180"
|
||||
defLogLevel = "error"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defThingsURL = "localhost:8181"
|
||||
envClientTLS = "MF_HTTP_ADAPTER_CLIENT_TLS"
|
||||
envCACerts = "MF_HTTP_ADAPTER_CA_CERTS"
|
||||
envPort = "MF_HTTP_ADAPTER_PORT"
|
||||
envLogLevel = "MF_HTTP_ADAPTER_LOG_LEVEL"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ThingsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
thingsURL string
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ThingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc, err := broker.Connect(cfg.NatsURL)
|
||||
nc, err := broker.Connect(cfg.natsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn, err := grpc.Dial(cfg.ThingsURL, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
conn := connectToThings(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
cc := thingsapi.NewClient(conn)
|
||||
@@ -88,8 +97,8 @@ func main() {
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
logger.Info(fmt.Sprintf("HTTP adapter service started, exposed port %s", cfg.Port))
|
||||
p := fmt.Sprintf(":%s", cfg.port)
|
||||
logger.Info(fmt.Sprintf("HTTP adapter service started on port %s", cfg.port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc))
|
||||
}()
|
||||
|
||||
@@ -102,3 +111,43 @@ func main() {
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("HTTP adapter terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
return config{
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
var opts []grpc.DialOption
|
||||
if cfg.clientTLS {
|
||||
if cfg.caCerts != "" {
|
||||
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(tpc))
|
||||
}
|
||||
} else {
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
+70
-33
@@ -2,56 +2,70 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
influxdata "github.com/influxdata/influxdb/client/v2"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/readers"
|
||||
"github.com/mainflux/mainflux/readers/api"
|
||||
"github.com/mainflux/mainflux/readers/influxdb"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/grpc"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
defThingsURL = "localhost:8181"
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDBName = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "8086"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
envLogLevel = "MF_INFLUX_READER_LOG_LEVEL"
|
||||
envPort = "MF_INFLUX_READER_PORT"
|
||||
envDBName = "MF_INFLUX_READER_DB_NAME"
|
||||
envDBHost = "MF_INFLUX_READER_DB_HOST"
|
||||
envDBPort = "MF_INFLUX_READER_DB_PORT"
|
||||
envDBUser = "MF_INFLUX_READER_DB_USER"
|
||||
envDBPass = "MF_INFLUX_READER_DB_PASS"
|
||||
envClientTLS = "MF_INFLUX_READER_CLIENT_TLS"
|
||||
envCACerts = "MF_INFLUX_READER_CA_CERTS"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ThingsURL string
|
||||
Port string
|
||||
DBName string
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
thingsURL string
|
||||
logLevel string
|
||||
port string
|
||||
dbName string
|
||||
dbHost string
|
||||
dbPort string
|
||||
dbUser string
|
||||
dbPass string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg, clientCfg := loadConfigs()
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
conn := connectToThings(cfg.ThingsURL, logger)
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
conn := connectToThings(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
tc := thingsapi.NewClient(conn)
|
||||
@@ -63,11 +77,7 @@ func main() {
|
||||
}
|
||||
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)
|
||||
}
|
||||
repo := newService(client, cfg.dbName, logger)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
go func() {
|
||||
@@ -76,44 +86,71 @@ func main() {
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
go startHTTPServer(repo, tc, cfg.Port, logger, errs)
|
||||
go startHTTPServer(repo, tc, cfg.port, logger, errs)
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("InfluxDB writer service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfigs() (config, influxdata.HTTPConfig) {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
cfg := config{
|
||||
ThingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
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),
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbName: mainflux.Env(envDBName, defDBName),
|
||||
dbHost: mainflux.Env(envDBHost, defDBHost),
|
||||
dbPort: mainflux.Env(envDBPort, defDBPort),
|
||||
dbUser: mainflux.Env(envDBUser, defDBUser),
|
||||
dbPass: mainflux.Env(envDBPass, defDBPass),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
}
|
||||
|
||||
clientCfg := influxdata.HTTPConfig{
|
||||
Addr: fmt.Sprintf("http://%s:%s", cfg.DBHost, cfg.DBPort),
|
||||
Username: cfg.DBUser,
|
||||
Password: cfg.DBPass,
|
||||
Addr: fmt.Sprintf("http://%s:%s", cfg.dbHost, cfg.dbPort),
|
||||
Username: cfg.dbUser,
|
||||
Password: cfg.dbPass,
|
||||
}
|
||||
|
||||
return cfg, clientCfg
|
||||
}
|
||||
|
||||
func connectToThings(url string, logger log.Logger) *grpc.ClientConn {
|
||||
conn, err := grpc.Dial(url, grpc.WithInsecure())
|
||||
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
var opts []grpc.DialOption
|
||||
if cfg.clientTLS {
|
||||
if cfg.caCerts != "" {
|
||||
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(tpc))
|
||||
}
|
||||
} else {
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func newService(client influxdata.Client, logger log.Logger) readers.MessageRepository {
|
||||
repo, _ := influxdb.New(client, "mainflux")
|
||||
func newService(client influxdata.Client, dbName string, logger logger.Logger) readers.MessageRepository {
|
||||
repo, err := influxdb.New(client, dbName)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create InfluxDB writer: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
repo = api.LoggingMiddleware(repo, logger)
|
||||
repo = api.MetricsMiddleware(
|
||||
repo,
|
||||
@@ -134,7 +171,7 @@ func newService(client influxdata.Client, logger log.Logger) readers.MessageRepo
|
||||
return repo
|
||||
}
|
||||
|
||||
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, port string, logger log.Logger, errs chan error) {
|
||||
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"))
|
||||
|
||||
+68
-37
@@ -9,54 +9,69 @@ 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"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/writers"
|
||||
"github.com/mainflux/mainflux/writers/influxdb"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
"github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
defNatsURL = nats.DefaultURL
|
||||
defPort = "8180"
|
||||
defPointName = "messages"
|
||||
defDBName = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "8086"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
queue = "influxdb-writer"
|
||||
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envPort = "MF_INFLUX_WRITER_PORT"
|
||||
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"
|
||||
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
|
||||
Port string
|
||||
DBName string
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
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 := log.New(os.Stdout)
|
||||
|
||||
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))
|
||||
@@ -71,7 +86,20 @@ func main() {
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
repo, err := influxdb.New(client, cfg.DBName)
|
||||
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)
|
||||
@@ -80,7 +108,7 @@ func main() {
|
||||
counter, latency := makeMetrics()
|
||||
repo = writers.LoggingMiddleware(repo, logger)
|
||||
repo = writers.MetricsMiddleware(repo, counter, latency)
|
||||
if err := writers.Start(nc, logger, repo); err != nil {
|
||||
if err := writers.Start(nc, repo, queue, logger); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to start message writer: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -100,13 +128,16 @@ func main() {
|
||||
|
||||
func loadConfigs() (config, influxdata.HTTPConfig) {
|
||||
cfg := config{
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
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),
|
||||
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{
|
||||
@@ -136,8 +167,8 @@ func makeMetrics() (*kitprometheus.Counter, *kitprometheus.Summary) {
|
||||
return counter, latency
|
||||
}
|
||||
|
||||
func startHTTPService(port string, logger log.Logger, errs chan error) {
|
||||
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))
|
||||
logger.Info(fmt.Sprintf("InfluxDB writer service started, exposed port %s", p))
|
||||
errs <- http.ListenAndServe(p, influxdb.MakeHandler())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
r "github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/lora"
|
||||
"github.com/mainflux/mainflux/lora/api"
|
||||
pub "github.com/mainflux/mainflux/lora/nats"
|
||||
mqttBroker "github.com/mainflux/mainflux/lora/paho"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux/lora/redis"
|
||||
"github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
defHTTPPort = "8180"
|
||||
defLoraMsgURL = "tcp://localhost:1883"
|
||||
defNatsURL = nats.DefaultURL
|
||||
defLogLevel = "error"
|
||||
defESURL = "localhost:6379"
|
||||
defESPass = ""
|
||||
defESDB = "0"
|
||||
defInstanceName = "lora"
|
||||
defRouteMapURL = "localhost:6379"
|
||||
defRouteMapPass = ""
|
||||
defRouteMapDB = "0"
|
||||
|
||||
envHTTPPort = "MF_LORA_ADAPTER_HTTP_PORT"
|
||||
envLoraMsgURL = "MF_LORA_ADAPTER_MESSAGES_URL"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_LORA_ADAPTER_LOG_LEVEL"
|
||||
envESURL = "MF_THINGS_ES_URL"
|
||||
envESPass = "MF_THINGS_ES_PASS"
|
||||
envESDB = "MF_THINGS_ES_DB"
|
||||
envInstanceName = "MF_LORA_ADAPTER_INSTANCE_NAME"
|
||||
envRouteMapURL = "MF_LORA_ADAPTER_ROUTEMAP_URL"
|
||||
envRouteMapPass = "MF_LORA_ADAPTER_ROUTEMAP_PASS"
|
||||
envRouteMapDB = "MF_LORA_ADAPTER_ROUTEMAP_DB"
|
||||
|
||||
loraServerTopic = "application/+/device/+/rx"
|
||||
|
||||
thingsRMPrefix = "thing"
|
||||
channelsRMPrefix = "channel"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
httpPort string
|
||||
loraMsgURL string
|
||||
natsURL string
|
||||
logLevel string
|
||||
esURL string
|
||||
esPass string
|
||||
esDB string
|
||||
instanceName string
|
||||
routeMapURL string
|
||||
routeMapPass string
|
||||
routeMapDB string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
natsConn := connectToNATS(cfg.natsURL, logger)
|
||||
defer natsConn.Close()
|
||||
|
||||
rmConn := connectToRedis(cfg.routeMapURL, cfg.routeMapPass, cfg.routeMapDB, logger)
|
||||
defer rmConn.Close()
|
||||
|
||||
esConn := connectToRedis(cfg.esURL, cfg.esPass, cfg.esDB, logger)
|
||||
defer esConn.Close()
|
||||
|
||||
publisher := pub.NewMessagePublisher(natsConn)
|
||||
|
||||
thingRM := newRouteMapRepositoy(rmConn, thingsRMPrefix, logger)
|
||||
chanRM := newRouteMapRepositoy(rmConn, channelsRMPrefix, logger)
|
||||
|
||||
mqttConn := connectToMQTTBroker(cfg.loraMsgURL, logger)
|
||||
|
||||
svc := lora.New(publisher, thingRM, chanRM)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "lora_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "lora_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
go subscribeToLoRaBroker(svc, mqttConn, logger)
|
||||
go subscribeToThingsES(svc, esConn, cfg.instanceName, logger)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(cfg, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("LoRa adapter terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
return config{
|
||||
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
loraMsgURL: mainflux.Env(envLoraMsgURL, defLoraMsgURL),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
esURL: mainflux.Env(envESURL, defESURL),
|
||||
esPass: mainflux.Env(envESPass, defESPass),
|
||||
esDB: mainflux.Env(envESDB, defESDB),
|
||||
instanceName: mainflux.Env(envInstanceName, defInstanceName),
|
||||
routeMapURL: mainflux.Env(envRouteMapURL, defRouteMapURL),
|
||||
routeMapPass: mainflux.Env(envRouteMapPass, defRouteMapPass),
|
||||
routeMapDB: mainflux.Env(envRouteMapDB, defRouteMapDB),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToNATS(url string, logger logger.Logger) *nats.Conn {
|
||||
conn, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger.Info("Connected to NATS")
|
||||
return conn
|
||||
}
|
||||
|
||||
func connectToMQTTBroker(loraURL string, logger logger.Logger) mqtt.Client {
|
||||
opts := mqtt.NewClientOptions()
|
||||
opts.AddBroker(loraURL)
|
||||
opts.SetUsername("")
|
||||
opts.SetPassword("")
|
||||
opts.SetOnConnectHandler(func(c mqtt.Client) {
|
||||
logger.Info("Connected to Lora MQTT broker")
|
||||
})
|
||||
opts.SetConnectionLostHandler(func(c mqtt.Client, err error) {
|
||||
logger.Error(fmt.Sprintf("MQTT connection lost: %s", err.Error()))
|
||||
os.Exit(1)
|
||||
})
|
||||
|
||||
client := mqtt.NewClient(opts)
|
||||
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to Lora MQTT broker: %s", token.Error()))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func connectToRedis(redisURL, redisPass, redisDB string, logger logger.Logger) *r.Client {
|
||||
db, err := strconv.Atoi(redisDB)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to redis: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return r.NewClient(&r.Options{
|
||||
Addr: redisURL,
|
||||
Password: redisPass,
|
||||
DB: db,
|
||||
})
|
||||
}
|
||||
|
||||
func subscribeToLoRaBroker(svc lora.Service, mc mqtt.Client, logger logger.Logger) {
|
||||
mqttBroker := mqttBroker.NewBroker(svc, mc, logger)
|
||||
logger.Info("Subscribed to Lora MQTT broker")
|
||||
if err := mqttBroker.Subscribe(loraServerTopic); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to subscribe to Lora MQTT broker: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func subscribeToThingsES(svc lora.Service, client *r.Client, consumer string, logger logger.Logger) {
|
||||
eventStore := redis.NewEventStore(svc, client, consumer, logger)
|
||||
logger.Info("Subscribed to Redis Event Store")
|
||||
if err := eventStore.Subscribe("mainflux.things"); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Lora-adapter service failed to subscribe to event sourcing: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
func newRouteMapRepositoy(client *r.Client, prefix string, logger logger.Logger) lora.RouteMapRepository {
|
||||
logger.Info("Connected to Redis Route map")
|
||||
return redis.NewRouteMapRepository(client, prefix)
|
||||
}
|
||||
|
||||
func startHTTPServer(cfg config, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.httpPort)
|
||||
logger.Info(fmt.Sprintf("Lora-adapter service started, exposed port %s", cfg.httpPort))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler())
|
||||
}
|
||||
+48
-11
@@ -10,14 +10,16 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/readers"
|
||||
"github.com/mainflux/mainflux/readers/api"
|
||||
"github.com/mainflux/mainflux/readers/mongodb"
|
||||
@@ -25,35 +27,48 @@ import (
|
||||
"github.com/mongodb/mongo-go-driver/mongo"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
defThingsURL = "localhost:8181"
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDBName = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "27017"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
envLogLevel = "MF_MONGO_READER_LOG_LEVEL"
|
||||
envPort = "MF_MONGO_READER_PORT"
|
||||
envDBName = "MF_MONGO_READER_DB_NAME"
|
||||
envDBHost = "MF_MONGO_READER_DB_HOST"
|
||||
envDBPort = "MF_MONGO_READER_DB_PORT"
|
||||
envClientTLS = "MF_MONGO_READER_CLIENT_TLS"
|
||||
envCACerts = "MF_MONGO_READER_CA_CERTS"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
thingsURL string
|
||||
logLevel string
|
||||
port string
|
||||
dbName string
|
||||
dbHost string
|
||||
dbPort string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfigs()
|
||||
logger := log.New(os.Stdout)
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
conn := connectToThings(cfg.thingsURL, logger)
|
||||
conn := connectToThings(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
tc := thingsapi.NewClient(conn)
|
||||
@@ -71,21 +86,29 @@ func main() {
|
||||
|
||||
go startHTTPServer(repo, tc, cfg.port, logger, errs)
|
||||
|
||||
err := <-errs
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("MongoDB reader service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfigs() config {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
return config{
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbName: mainflux.Env(envDBName, defDBName),
|
||||
dbHost: mainflux.Env(envDBHost, defDBHost),
|
||||
dbPort: mainflux.Env(envDBPort, defDBPort),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToMongoDB(host, port, name string, logger log.Logger) *mongo.Database {
|
||||
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))
|
||||
@@ -95,17 +118,31 @@ func connectToMongoDB(host, port, name string, logger log.Logger) *mongo.Databas
|
||||
return client.Database(name)
|
||||
}
|
||||
|
||||
func connectToThings(url string, logger log.Logger) *grpc.ClientConn {
|
||||
conn, err := grpc.Dial(url, grpc.WithInsecure())
|
||||
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
var opts []grpc.DialOption
|
||||
if cfg.clientTLS {
|
||||
if cfg.caCerts != "" {
|
||||
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(tpc))
|
||||
}
|
||||
} else {
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func newService(db *mongo.Database, logger log.Logger) readers.MessageRepository {
|
||||
func newService(db *mongo.Database, logger logger.Logger) readers.MessageRepository {
|
||||
repo := mongodb.New(db)
|
||||
repo = api.LoggingMiddleware(repo, logger)
|
||||
repo = api.MetricsMiddleware(
|
||||
@@ -127,8 +164,8 @@ func newService(db *mongo.Database, logger log.Logger) readers.MessageRepository
|
||||
return repo
|
||||
}
|
||||
|
||||
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, port string, logger log.Logger, errs chan error) {
|
||||
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"))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(repo, tc, "mongodb-reader"))
|
||||
}
|
||||
|
||||
+36
-27
@@ -10,6 +10,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -17,40 +18,47 @@ 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/writers"
|
||||
mongodb "github.com/mainflux/mainflux/writers/mongodb"
|
||||
"github.com/mainflux/mainflux/writers/mongodb"
|
||||
"github.com/mongodb/mongo-go-driver/mongo"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
"github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
defNatsURL = nats.DefaultURL
|
||||
defPort = "8180"
|
||||
defDBName = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "27017"
|
||||
queue = "mongodb-writer"
|
||||
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envPort = "MF_MONGO_WRITER_PORT"
|
||||
envDBName = "MF_MONGO_WRITER_DB_NAME"
|
||||
envDBHost = "MF_MONGO_WRITER_DB_HOST"
|
||||
envDBPort = "MF_MONGO_WRITER_DB_PORT"
|
||||
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
|
||||
Port string
|
||||
DBName string
|
||||
DBHost string
|
||||
DBPort string
|
||||
NatsURL string
|
||||
LogLevel string
|
||||
Port string
|
||||
DBName string
|
||||
DBHost string
|
||||
DBPort string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfigs()
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
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))
|
||||
@@ -70,7 +78,7 @@ func main() {
|
||||
counter, latency := makeMetrics()
|
||||
repo = writers.LoggingMiddleware(repo, logger)
|
||||
repo = writers.MetricsMiddleware(repo, counter, latency)
|
||||
if err := writers.Start(nc, logger, repo); err != nil {
|
||||
if err := writers.Start(nc, repo, queue, logger); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to start message writer: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -90,11 +98,12 @@ func main() {
|
||||
|
||||
func loadConfigs() config {
|
||||
return config{
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +125,7 @@ func makeMetrics() (*kitprometheus.Counter, *kitprometheus.Summary) {
|
||||
return counter, latency
|
||||
}
|
||||
|
||||
func startHTTPService(port string, logger log.Logger, errs chan error) {
|
||||
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())
|
||||
|
||||
+24
-13
@@ -9,13 +9,14 @@ 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"
|
||||
"github.com/mainflux/mainflux/normalizer/api"
|
||||
"github.com/mainflux/mainflux/normalizer/nats"
|
||||
@@ -26,25 +27,27 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defNatsURL string = broker.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 := broker.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
@@ -89,3 +92,11 @@ func main() {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
+181
-54
@@ -10,20 +10,26 @@ package main
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"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"
|
||||
@@ -31,51 +37,94 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "things"
|
||||
defHTTPPort = "8180"
|
||||
defGRPCPort = "8181"
|
||||
defUsersURL = "localhost:8181"
|
||||
envDBHost = "MF_THINGS_DB_HOST"
|
||||
envDBPort = "MF_THINGS_DB_PORT"
|
||||
envDBUser = "MF_THINGS_DB_USER"
|
||||
envDBPass = "MF_THINGS_DB_PASS"
|
||||
envDBName = "MF_THINGS_DB"
|
||||
envHTTPPort = "MF_THINGS_HTTP_PORT"
|
||||
envGRPCPort = "MF_THINGS_GRPC_PORT"
|
||||
envUsersURL = "MF_USERS_URL"
|
||||
defLogLevel = "error"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "things"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defCacheURL = "localhost:6379"
|
||||
defCachePass = ""
|
||||
defCacheDB = "0"
|
||||
defESURL = "localhost:6379"
|
||||
defESPass = ""
|
||||
defESDB = "0"
|
||||
defHTTPPort = "8180"
|
||||
defGRPCPort = "8181"
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defUsersURL = "localhost:8181"
|
||||
envLogLevel = "MF_THINGS_LOG_LEVEL"
|
||||
envDBHost = "MF_THINGS_DB_HOST"
|
||||
envDBPort = "MF_THINGS_DB_PORT"
|
||||
envDBUser = "MF_THINGS_DB_USER"
|
||||
envDBPass = "MF_THINGS_DB_PASS"
|
||||
envDBName = "MF_THINGS_DB"
|
||||
envDBSSLMode = "MF_THINGS_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_THINGS_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_THINGS_DB_SSL_KEY"
|
||||
envDBSSLRootCert = "MF_THINGS_DB_SSL_ROOT_CERT"
|
||||
envClientTLS = "MF_THINGS_CLIENT_TLS"
|
||||
envCACerts = "MF_THINGS_CA_CERTS"
|
||||
envCacheURL = "MF_THINGS_CACHE_URL"
|
||||
envCachePass = "MF_THINGS_CACHE_PASS"
|
||||
envCacheDB = "MF_THINGS_CACHE_DB"
|
||||
envESURL = "MF_THINGS_ES_URL"
|
||||
envESPass = "MF_THINGS_ES_PASS"
|
||||
envESDB = "MF_THINGS_ES_DB"
|
||||
envHTTPPort = "MF_THINGS_HTTP_PORT"
|
||||
envGRPCPort = "MF_THINGS_GRPC_PORT"
|
||||
envUsersURL = "MF_USERS_URL"
|
||||
envServerCert = "MF_THINGS_SERVER_CERT"
|
||||
envServerKey = "MF_THINGS_SERVER_KEY"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
HTTPPort string
|
||||
GRPCPort string
|
||||
UsersURL string
|
||||
logLevel string
|
||||
dbConfig postgres.Config
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
cacheURL string
|
||||
cachePass string
|
||||
cacheDB string
|
||||
esURL string
|
||||
esPass string
|
||||
esDB string
|
||||
httpPort string
|
||||
grpcPort string
|
||||
usersURL string
|
||||
serverCert string
|
||||
serverKey string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
cacheClient := connectToRedis(cfg.cacheURL, cfg.cachePass, cfg.cacheDB, logger)
|
||||
|
||||
db := connectToDB(cfg, logger)
|
||||
esClient := connectToRedis(cfg.esURL, cfg.esPass, cfg.esDB, logger)
|
||||
|
||||
db := connectToDB(cfg.dbConfig, logger)
|
||||
defer db.Close()
|
||||
|
||||
conn := connectToUsersService(cfg.UsersURL, logger)
|
||||
conn := connectToUsers(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
svc := newService(conn, db, logger)
|
||||
svc := newService(conn, db, cacheClient, esClient, logger)
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
|
||||
go startGRPCServer(svc, cfg.GRPCPort, logger, errs)
|
||||
go startHTTPServer(svc, cfg, logger, errs)
|
||||
go startGRPCServer(svc, cfg, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
@@ -83,25 +132,63 @@ func main() {
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err := <-errs
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Things service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
Host: mainflux.Env(envDBHost, defDBHost),
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Name: mainflux.Env(envDBName, defDBName),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
|
||||
}
|
||||
|
||||
return config{
|
||||
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),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
dbConfig: dbConfig,
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
cacheURL: mainflux.Env(envCacheURL, defCacheURL),
|
||||
cachePass: mainflux.Env(envCachePass, defCachePass),
|
||||
cacheDB: mainflux.Env(envCacheDB, defCacheDB),
|
||||
esURL: mainflux.Env(envESURL, defESURL),
|
||||
esPass: mainflux.Env(envESPass, defESPass),
|
||||
esDB: mainflux.Env(envESDB, defESDB),
|
||||
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
grpcPort: mainflux.Env(envGRPCPort, defGRPCPort),
|
||||
usersURL: mainflux.Env(envUsersURL, defUsersURL),
|
||||
serverCert: mainflux.Env(envServerCert, defServerCert),
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToDB(cfg config, logger log.Logger) *sql.DB {
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
func connectToRedis(cacheURL, cachePass string, cacheDB string, logger logger.Logger) *redis.Client {
|
||||
db, err := strconv.Atoi(cacheDB)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to cache: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return redis.NewClient(&redis.Options{
|
||||
Addr: cacheURL,
|
||||
Password: cachePass,
|
||||
DB: db,
|
||||
})
|
||||
}
|
||||
|
||||
func connectToDB(dbConfig postgres.Config, logger logger.Logger) *sql.DB {
|
||||
db, err := postgres.Connect(dbConfig)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
@@ -109,22 +196,41 @@ func connectToDB(cfg config, logger log.Logger) *sql.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
func connectToUsersService(usersAddr string, logger log.Logger) *grpc.ClientConn {
|
||||
conn, err := grpc.Dial(usersAddr, grpc.WithInsecure())
|
||||
func connectToUsers(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
var opts []grpc.DialOption
|
||||
if cfg.clientTLS {
|
||||
if cfg.caCerts != "" {
|
||||
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create tls credentials: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(tpc))
|
||||
}
|
||||
} else {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.usersURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func newService(conn *grpc.ClientConn, db *sql.DB, logger log.Logger) things.Service {
|
||||
func newService(conn *grpc.ClientConn, db *sql.DB, cacheClient *redis.Client, esClient *redis.Client, logger logger.Logger) things.Service {
|
||||
users := usersapi.NewClient(conn)
|
||||
thingsRepo := postgres.NewThingRepository(db, logger)
|
||||
channelsRepo := postgres.NewChannelRepository(db, logger)
|
||||
chanCache := rediscache.NewChannelCache(cacheClient)
|
||||
thingCache := rediscache.NewThingCache(cacheClient)
|
||||
idp := uuid.New()
|
||||
|
||||
svc := things.New(users, thingsRepo, channelsRepo, idp)
|
||||
svc := things.New(users, thingsRepo, channelsRepo, chanCache, thingCache, idp)
|
||||
svc = rediscache.NewEventStoreMiddleware(svc, esClient)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
@@ -144,20 +250,41 @@ func newService(conn *grpc.ClientConn, db *sql.DB, logger log.Logger) things.Ser
|
||||
return svc
|
||||
}
|
||||
|
||||
func startHTTPServer(svc things.Service, port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("Things service started, exposed port %s", port))
|
||||
func startHTTPServer(svc things.Service, cfg config, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.httpPort)
|
||||
if cfg.serverCert != "" || cfg.serverKey != "" {
|
||||
logger.Info(fmt.Sprintf("Things service started using https on port %s with cert %s key %s",
|
||||
cfg.httpPort, cfg.serverCert, cfg.serverKey))
|
||||
errs <- http.ListenAndServeTLS(p, cfg.serverCert, cfg.serverKey, httpapi.MakeHandler(svc))
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Things service started using http on port %s", cfg.httpPort))
|
||||
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc))
|
||||
}
|
||||
|
||||
func startGRPCServer(svc things.Service, port string, logger log.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
func startGRPCServer(svc things.Service, cfg config, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.grpcPort)
|
||||
listener, err := net.Listen("tcp", p)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to listen on port %s: %s", port, err))
|
||||
logger.Error(fmt.Sprintf("Failed to listen on port %s: %s", cfg.grpcPort, err))
|
||||
os.Exit(1)
|
||||
}
|
||||
server := grpc.NewServer()
|
||||
|
||||
var server *grpc.Server
|
||||
if cfg.serverCert != "" || cfg.serverKey != "" {
|
||||
creds, err := credentials.NewServerTLSFromFile(cfg.serverCert, cfg.serverKey)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to load things certificates: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Things gRPC service started using https on port %s with cert %s key %s",
|
||||
cfg.grpcPort, cfg.serverCert, cfg.serverKey))
|
||||
server = grpc.NewServer(grpc.Creds(creds))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Things gRPC service started using http on port %s", cfg.grpcPort))
|
||||
server = grpc.NewServer()
|
||||
}
|
||||
|
||||
mainflux.RegisterThingsServiceServer(server, grpcapi.NewServer(svc))
|
||||
logger.Info(fmt.Sprintf("Things gRPC service started, exposed port %s", port))
|
||||
errs <- server.Serve(listener)
|
||||
}
|
||||
|
||||
+97
-48
@@ -10,15 +10,18 @@ package main
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
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"
|
||||
@@ -31,48 +34,63 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "users"
|
||||
defHTTPPort = "8180"
|
||||
defGRPCPort = "8181"
|
||||
defSecret = "users"
|
||||
envDBHost = "MF_USERS_DB_HOST"
|
||||
envDBPort = "MF_USERS_DB_PORT"
|
||||
envDBUser = "MF_USERS_DB_USER"
|
||||
envDBPass = "MF_USERS_DB_PASS"
|
||||
envDBName = "MF_USERS_DB"
|
||||
envHTTPPort = "MF_USERS_HTTP_PORT"
|
||||
envGRPCPort = "MF_USERS_GRPC_PORT"
|
||||
envSecret = "MF_USERS_SECRET"
|
||||
defLogLevel = "error"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "users"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defHTTPPort = "8180"
|
||||
defGRPCPort = "8181"
|
||||
defSecret = "users"
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
envLogLevel = "MF_USERS_LOG_LEVEL"
|
||||
envDBHost = "MF_USERS_DB_HOST"
|
||||
envDBPort = "MF_USERS_DB_PORT"
|
||||
envDBUser = "MF_USERS_DB_USER"
|
||||
envDBPass = "MF_USERS_DB_PASS"
|
||||
envDBName = "MF_USERS_DB"
|
||||
envDBSSLMode = "MF_USERS_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_USERS_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_USERS_DB_SSL_KEY"
|
||||
envDBSSLRootCert = "MF_USERS_DB_SSL_ROOT_CERT"
|
||||
envHTTPPort = "MF_USERS_HTTP_PORT"
|
||||
envGRPCPort = "MF_USERS_GRPC_PORT"
|
||||
envSecret = "MF_USERS_SECRET"
|
||||
envServerCert = "MF_USERS_SERVER_CERT"
|
||||
envServerKey = "MF_USERS_SERVER_KEY"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
HTTPPort string
|
||||
GRPCPort string
|
||||
Secret string
|
||||
logLevel string
|
||||
dbConfig postgres.Config
|
||||
httpPort string
|
||||
grpcPort string
|
||||
secret string
|
||||
serverCert string
|
||||
serverKey string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
db := connectToDB(cfg, logger)
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
db := connectToDB(cfg.dbConfig, logger)
|
||||
defer db.Close()
|
||||
|
||||
svc := newService(db, cfg.Secret, logger)
|
||||
svc := newService(db, cfg.secret, logger)
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg.HTTPPort, logger, errs)
|
||||
go startGRPCServer(svc, cfg.GRPCPort, logger, errs)
|
||||
go startHTTPServer(svc, cfg.httpPort, cfg.serverCert, cfg.serverKey, logger, errs)
|
||||
go startGRPCServer(svc, cfg.grpcPort, cfg.serverCert, cfg.serverKey, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
@@ -80,25 +98,37 @@ func main() {
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err := <-errs
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Users service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
Host: mainflux.Env(envDBHost, defDBHost),
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Name: mainflux.Env(envDBName, defDBName),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
|
||||
}
|
||||
|
||||
return config{
|
||||
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),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
dbConfig: dbConfig,
|
||||
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
grpcPort: mainflux.Env(envGRPCPort, defGRPCPort),
|
||||
secret: mainflux.Env(envSecret, defSecret),
|
||||
serverCert: mainflux.Env(envServerCert, defServerCert),
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToDB(cfg config, logger log.Logger) *sql.DB {
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
func connectToDB(dbConfig postgres.Config, logger logger.Logger) *sql.DB {
|
||||
db, err := postgres.Connect(dbConfig)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
@@ -106,7 +136,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)
|
||||
@@ -131,19 +161,38 @@ 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, certFile string, keyFile 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))
|
||||
if certFile != "" || keyFile != "" {
|
||||
logger.Info(fmt.Sprintf("Users service started using https, cert %s key %s, exposed port %s", certFile, keyFile, port))
|
||||
errs <- http.ListenAndServeTLS(p, certFile, keyFile, httpapi.MakeHandler(svc, logger))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Users service started using http, exposed port %s", port))
|
||||
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc, logger))
|
||||
}
|
||||
}
|
||||
|
||||
func startGRPCServer(svc users.Service, port string, logger log.Logger, errs chan error) {
|
||||
func startGRPCServer(svc users.Service, port string, certFile string, keyFile string, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
listener, err := net.Listen("tcp", p)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to listen on port %s: %s", port, err))
|
||||
}
|
||||
server := grpc.NewServer()
|
||||
|
||||
var server *grpc.Server
|
||||
if certFile != "" || keyFile != "" {
|
||||
creds, err := credentials.NewServerTLSFromFile(certFile, keyFile)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to load users certificates: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Users gRPC service started using https on port %s with cert %s key %s", port, certFile, keyFile))
|
||||
server = grpc.NewServer(grpc.Creds(creds))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Users gRPC service started using http on port %s", port))
|
||||
server = grpc.NewServer()
|
||||
}
|
||||
|
||||
mainflux.RegisterUsersServiceServer(server, grpcapi.NewServer(svc))
|
||||
logger.Info(fmt.Sprintf("Users gRPC service started, exposed port %s", port))
|
||||
errs <- server.Serve(listener)
|
||||
|
||||
+85
-32
@@ -9,14 +9,16 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"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"
|
||||
@@ -24,48 +26,114 @@ import (
|
||||
broker "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defPort = "8180"
|
||||
defLogLevel = "error"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defThingsURL = "localhost:8181"
|
||||
envClientTLS = "MF_WS_ADAPTER_CLIENT_TLS"
|
||||
envCACerts = "MF_WS_ADAPTER_CA_CERTS"
|
||||
envPort = "MF_WS_ADAPTER_PORT"
|
||||
envLogLevel = "MF_WS_ADAPTER_LOG_LEVEL"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ThingsURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
thingsURL string
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
ThingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout)
|
||||
|
||||
nc, err := broker.Connect(cfg.NatsURL)
|
||||
nc, err := broker.Connect(cfg.natsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn, 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)
|
||||
}
|
||||
conn := connectToThings(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
cc := thingsapi.NewClient(conn)
|
||||
pubsub := nats.New(nc)
|
||||
svc := newService(pubsub, logger)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.port)
|
||||
logger.Info(fmt.Sprintf("WebSocket adapter service started, exposed port %s", cfg.port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc, logger))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("WebSocket adapter terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
return config{
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
var opts []grpc.DialOption
|
||||
if cfg.clientTLS {
|
||||
if cfg.caCerts != "" {
|
||||
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(tpc))
|
||||
}
|
||||
} else {
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
func newService(pubsub adapter.Service, logger logger.Logger) adapter.Service {
|
||||
svc := adapter.New(pubsub)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
@@ -84,20 +152,5 @@ func main() {
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
logger.Info(fmt.Sprintf("WebSocket adapter service started, exposed port %s", cfg.Port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc, logger))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("WebSocket adapter terminated: %s", err))
|
||||
return svc
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# 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 | Service listening port | 5683 |
|
||||
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
|
||||
| MF_THINGS_URL | Things service URL | localhost:8181 |
|
||||
| MF_COAP_ADAPTER_LOG_LEVEL | Service log level | error |
|
||||
| MF_COAP_ADAPTER_CLIENT_TLS | Flag that indicates if TLS should be turned on | false |
|
||||
| MF_COAP_ADAPTER_CA_CERTS | Path to trusted CAs in PEM format | |
|
||||
| MF_COAP_ADAPTER_PING_PERIOD | Hours between 1 and 24 to ping client with ACK message | 12 |
|
||||
|
||||
## Deployment
|
||||
|
||||
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:[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_THINGS_URL: [Things service URL]
|
||||
MF_COAP_ADAPTER_LOG_LEVEL: [Service log level]
|
||||
MF_COAP_ADAPTER_CLIENT_TLS: [Flag that indicates if TLS should be turned on]
|
||||
MF_COAP_ADAPTER_CA_CERTS: [Path to trusted CAs in PEM format]
|
||||
MF_COAP_ADAPTER_PING_PERIOD: [Hours between 1 and 24 to ping client with ACK message]
|
||||
```
|
||||
|
||||
Running this service outside of container requires working instance of the NATS service.
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
|
||||
# compile the http
|
||||
make coap
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_THINGS_URL=[Things service URL] MF_NATS_URL=[NATS instance URL] MF_COAP_ADAPTER_PORT=[Service HTTP port] MF_COAP_ADAPTER_LOG_LEVEL=[Service log level] MF_COAP_ADAPTER_CLIENT_TLS=[Flag that indicates if TLS should be turned on] MF_COAP_ADAPTER_CA_CERTS=[Path to trusted CAs in PEM format] MF_COAP_ADAPTER_PING_PERIOD: [Hours between 1 and 24 to ping client with ACK message] $GOBIN/mainflux-coap
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
If CoAP adapter is running locally (on default 5683 port), a valid URL would be: `coap://localhost/channels/<channel_id>/messages?authorization=<thing_auth_key>`.
|
||||
Since CoAP protocol does not support `Authorization` header (option) and options have limited size, in order to send CoAP messages, valid `authorization` value must be present in `Uri-Query` option.
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Package coap contains the domain concept definitions needed to support
|
||||
// Mainflux coap adapter service functionality. All constant values are taken
|
||||
// from RFC, and could be adjusted based on specific use case.
|
||||
package coap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
chanID = "id"
|
||||
keyHeader = "key"
|
||||
|
||||
// AckRandomFactor is default ACK coefficient.
|
||||
AckRandomFactor = 1.5
|
||||
// AckTimeout is the amount of time to wait for a response.
|
||||
AckTimeout = 2000 * time.Millisecond
|
||||
// MaxRetransmit is the maximum number of times a message will be retransmitted.
|
||||
MaxRetransmit = 4
|
||||
)
|
||||
|
||||
var (
|
||||
errBadOption = errors.New("bad option")
|
||||
// ErrFailedMessagePublish indicates that message publishing failed.
|
||||
ErrFailedMessagePublish = errors.New("failed to publish message")
|
||||
|
||||
// ErrFailedSubscription indicates that client couldn't subscribe to specified channel.
|
||||
ErrFailedSubscription = errors.New("failed to subscribe to a channel")
|
||||
|
||||
// ErrFailedConnection indicates that service couldn't connect to message broker.
|
||||
ErrFailedConnection = errors.New("failed to connect to message broker")
|
||||
)
|
||||
|
||||
// Broker represents NATS broker instance.
|
||||
type Broker interface {
|
||||
mainflux.MessagePublisher
|
||||
|
||||
// Subscribes to channel with specified id, subtopic and adds subscription to
|
||||
// service map of subscriptions under given ID.
|
||||
Subscribe(string, string, string, *Observer) error
|
||||
}
|
||||
|
||||
// Service specifies coap service API.
|
||||
type Service interface {
|
||||
Broker
|
||||
// Unsubscribe method is used to stop observing resource.
|
||||
Unsubscribe(string)
|
||||
}
|
||||
|
||||
var _ Service = (*adapterService)(nil)
|
||||
|
||||
type adapterService struct {
|
||||
pubsub Broker
|
||||
obs map[string]*Observer
|
||||
obsLock sync.Mutex
|
||||
}
|
||||
|
||||
// New instantiates the CoAP adapter implementation.
|
||||
func New(pubsub Broker, responses <-chan string) Service {
|
||||
as := &adapterService{
|
||||
pubsub: pubsub,
|
||||
obs: make(map[string]*Observer),
|
||||
obsLock: sync.Mutex{},
|
||||
}
|
||||
|
||||
go as.listenResponses(responses)
|
||||
return as
|
||||
}
|
||||
|
||||
func (svc *adapterService) get(obsID string) (*Observer, bool) {
|
||||
svc.obsLock.Lock()
|
||||
defer svc.obsLock.Unlock()
|
||||
|
||||
val, ok := svc.obs[obsID]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (svc *adapterService) put(obsID string, o *Observer) {
|
||||
svc.obsLock.Lock()
|
||||
defer svc.obsLock.Unlock()
|
||||
|
||||
val, ok := svc.obs[obsID]
|
||||
if ok {
|
||||
close(val.Cancel)
|
||||
}
|
||||
|
||||
svc.obs[obsID] = o
|
||||
}
|
||||
|
||||
func (svc *adapterService) remove(obsID string) {
|
||||
svc.obsLock.Lock()
|
||||
defer svc.obsLock.Unlock()
|
||||
|
||||
val, ok := svc.obs[obsID]
|
||||
if ok {
|
||||
close(val.Cancel)
|
||||
delete(svc.obs, obsID)
|
||||
}
|
||||
}
|
||||
|
||||
// ListenResponses method handles ACK messages received from client.
|
||||
func (svc *adapterService) listenResponses(responses <-chan string) {
|
||||
for {
|
||||
id := <-responses
|
||||
|
||||
val, ok := svc.get(id)
|
||||
if ok {
|
||||
val.StoreExpired(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *adapterService) Publish(msg mainflux.RawMessage) error {
|
||||
if err := svc.pubsub.Publish(msg); err != nil {
|
||||
switch err {
|
||||
case broker.ErrConnectionClosed, broker.ErrInvalidConnection:
|
||||
return ErrFailedConnection
|
||||
default:
|
||||
return ErrFailedMessagePublish
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *adapterService) Subscribe(chanID, subtopic, obsID string, o *Observer) error {
|
||||
if err := svc.pubsub.Subscribe(chanID, subtopic, obsID, o); err != nil {
|
||||
return ErrFailedSubscription
|
||||
}
|
||||
|
||||
// Put method removes Observer if already exists.
|
||||
svc.put(obsID, o)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *adapterService) Unsubscribe(obsID string) {
|
||||
svc.remove(obsID)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Package api contains API-related concerns: endpoint definitions, middlewares
|
||||
// and all resource representations.
|
||||
package api
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ coap.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc coap.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the adapter.
|
||||
func LoggingMiddleware(svc coap.Service, logger log.Logger) coap.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
destChannel := msg.Channel
|
||||
if msg.Subtopic != "" {
|
||||
destChannel = fmt.Sprintf("%s.%s", destChannel, msg.Subtopic)
|
||||
}
|
||||
message := fmt.Sprintf("Method publish to channel %s took %s to complete", destChannel, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Publish(msg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Subscribe(chanID, subtopic, obsID string, o *coap.Observer) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
destChannel := chanID
|
||||
if subtopic != "" {
|
||||
destChannel = fmt.Sprintf("%s.%s", destChannel, subtopic)
|
||||
}
|
||||
message := fmt.Sprintf("Method subscribe to channel %s for client %s took %s to complete", destChannel, obsID, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
}
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Subscribe(chanID, subtopic, obsID, o)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Unsubscribe(obsID string) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method unsubscribe for the client %s took %s to complete without errors.", obsID, time.Since(begin))
|
||||
lm.logger.Info(fmt.Sprintf(message))
|
||||
}(time.Now())
|
||||
|
||||
lm.svc.Unsubscribe(obsID)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// +build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
)
|
||||
|
||||
var _ coap.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc coap.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments adapter by tracking request count and latency.
|
||||
func MetricsMiddleware(svc coap.Service, counter metrics.Counter, latency metrics.Histogram) coap.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Publish(msg mainflux.RawMessage) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "publish").Add(1)
|
||||
mm.latency.With("method", "publish").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Publish(msg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Subscribe(chanID, subtopic, clientID string, o *coap.Observer) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "subscribe").Add(1)
|
||||
mm.latency.With("method", "subscribe").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Subscribe(chanID, subtopic, clientID, o)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Unsubscribe(clientID string) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "unsubscribe").Add(1)
|
||||
mm.latency.With("method", "unsubscribe").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
mm.svc.Unsubscribe(clientID)
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
mux "github.com/dereulenspiegel/coap-mux"
|
||||
gocoap "github.com/dustin/go-coap"
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
const protocol = "coap"
|
||||
|
||||
var (
|
||||
errBadRequest = errors.New("bad request")
|
||||
errBadOption = errors.New("bad option")
|
||||
errMalformedSubtopic = errors.New("malformed subtopic")
|
||||
)
|
||||
|
||||
var (
|
||||
auth mainflux.ThingsServiceClient
|
||||
logger log.Logger
|
||||
pingPeriod time.Duration
|
||||
)
|
||||
|
||||
type handler func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message
|
||||
|
||||
func notFoundHandler(l *net.UDPConn, a *net.UDPAddr, m *gocoap.Message) *gocoap.Message {
|
||||
if m.IsConfirmable() {
|
||||
return &gocoap.Message{
|
||||
Type: gocoap.Acknowledgement,
|
||||
Code: gocoap.NotFound,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//MakeHTTPHandler creates handler for version endpoint.
|
||||
func MakeHTTPHandler() http.Handler {
|
||||
b := bone.New()
|
||||
b.GetFunc("/version", mainflux.Version(protocol))
|
||||
b.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// MakeCOAPHandler creates handler for CoAP messages.
|
||||
func MakeCOAPHandler(svc coap.Service, tc mainflux.ThingsServiceClient, l log.Logger, responses chan<- string, pp time.Duration) gocoap.Handler {
|
||||
auth = tc
|
||||
logger = l
|
||||
pingPeriod = pp
|
||||
r := mux.NewRouter()
|
||||
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(receive(svc))).Methods(gocoap.POST)
|
||||
r.Handle("/channels/{id}/messages/{subtopic:[^?]*}", gocoap.FuncHandler(receive(svc))).Methods(gocoap.POST)
|
||||
r.Handle("/channels/{id}/messages", gocoap.FuncHandler(observe(svc, responses)))
|
||||
r.Handle("/channels/{id}/messages/{subtopic:[^?]*}", gocoap.FuncHandler(observe(svc, responses)))
|
||||
r.NotFoundHandler = gocoap.FuncHandler(notFoundHandler)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func authorize(msg *gocoap.Message, res *gocoap.Message, cid string) (string, error) {
|
||||
// Device Key is passed as Uri-Query parameter, which option ID is 15 (0xf).
|
||||
key, err := authKey(msg.Option(gocoap.URIQuery))
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errBadOption:
|
||||
res.Code = gocoap.BadOption
|
||||
case errBadRequest:
|
||||
res.Code = gocoap.BadRequest
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, err := auth.CanAccess(ctx, &mainflux.AccessReq{Token: key, ChanID: cid})
|
||||
|
||||
if err != nil {
|
||||
e, ok := status.FromError(err)
|
||||
if ok {
|
||||
switch e.Code() {
|
||||
case codes.PermissionDenied:
|
||||
res.Code = gocoap.Forbidden
|
||||
default:
|
||||
res.Code = gocoap.ServiceUnavailable
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
res.Code = gocoap.InternalServerError
|
||||
}
|
||||
return id.GetValue(), nil
|
||||
}
|
||||
|
||||
func fmtSubtopic(subtopic string) (string, error) {
|
||||
if subtopic == "" {
|
||||
return subtopic, nil
|
||||
}
|
||||
|
||||
subtopic = strings.Replace(subtopic, "/", ".", -1)
|
||||
|
||||
elems := strings.Split(subtopic, ".")
|
||||
filteredElems := []string{}
|
||||
for _, elem := range elems {
|
||||
if elem == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(elem) > 1 && (strings.Contains(elem, "*") || strings.Contains(elem, ">")) {
|
||||
return "", errMalformedSubtopic
|
||||
}
|
||||
|
||||
filteredElems = append(filteredElems, elem)
|
||||
}
|
||||
|
||||
subtopic = strings.Join(filteredElems, ".")
|
||||
|
||||
return subtopic, nil
|
||||
}
|
||||
|
||||
func receive(svc coap.Service) handler {
|
||||
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
|
||||
// By default message is NonConfirmable, so
|
||||
// NonConfirmable response is sent back.
|
||||
res := &gocoap.Message{
|
||||
Type: gocoap.NonConfirmable,
|
||||
// According to https://tools.ietf.org/html/rfc7252#page-47: If the POST
|
||||
// succeeds but does not result in a new resource being created on the
|
||||
// server, the response SHOULD have a 2.04 (Changed) Response Code.
|
||||
Code: gocoap.Changed,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
|
||||
if msg.IsConfirmable() {
|
||||
res.Type = gocoap.Acknowledgement
|
||||
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
|
||||
if len(msg.Payload) == 0 {
|
||||
res.Code = gocoap.BadRequest
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
chanID := mux.Var(msg, "id")
|
||||
if chanID == "" {
|
||||
res.Code = gocoap.NotFound
|
||||
return res
|
||||
}
|
||||
|
||||
subtopic, err := fmtSubtopic(mux.Var(msg, "subtopic"))
|
||||
if err != nil {
|
||||
res.Code = gocoap.BadRequest
|
||||
return res
|
||||
}
|
||||
|
||||
publisher, err := authorize(msg, res, chanID)
|
||||
if err != nil {
|
||||
res.Code = gocoap.Forbidden
|
||||
return res
|
||||
}
|
||||
|
||||
rawMsg := mainflux.RawMessage{
|
||||
Channel: chanID,
|
||||
Subtopic: subtopic,
|
||||
Publisher: publisher,
|
||||
Protocol: protocol,
|
||||
Payload: msg.Payload,
|
||||
}
|
||||
|
||||
if err := svc.Publish(rawMsg); err != nil {
|
||||
res.Code = gocoap.InternalServerError
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func observe(svc coap.Service, responses chan<- string) handler {
|
||||
return func(conn *net.UDPConn, addr *net.UDPAddr, msg *gocoap.Message) *gocoap.Message {
|
||||
res := &gocoap.Message{
|
||||
Type: gocoap.Acknowledgement,
|
||||
Code: gocoap.Content,
|
||||
MessageID: msg.MessageID,
|
||||
Token: msg.Token,
|
||||
Payload: []byte{},
|
||||
}
|
||||
res.SetOption(gocoap.ContentFormat, gocoap.AppJSON)
|
||||
|
||||
chanID := mux.Var(msg, "id")
|
||||
if chanID == "" {
|
||||
res.Code = gocoap.NotFound
|
||||
return res
|
||||
}
|
||||
|
||||
subtopic, err := fmtSubtopic(mux.Var(msg, "subtopic"))
|
||||
if err != nil {
|
||||
res.Code = gocoap.BadRequest
|
||||
return res
|
||||
}
|
||||
|
||||
publisher, err := authorize(msg, res, chanID)
|
||||
if err != nil {
|
||||
res.Code = gocoap.Forbidden
|
||||
logger.Warn(fmt.Sprintf("Failed to authorize: %s", err))
|
||||
return res
|
||||
}
|
||||
|
||||
obsID := fmt.Sprintf("%x-%s-%s", msg.Token, publisher, chanID)
|
||||
|
||||
if msg.Type == gocoap.Acknowledgement {
|
||||
responses <- obsID
|
||||
return nil
|
||||
}
|
||||
|
||||
if value, ok := msg.Option(gocoap.Observe).(uint32); (ok && value == 1) || msg.Type == gocoap.Reset {
|
||||
svc.Unsubscribe(obsID)
|
||||
}
|
||||
|
||||
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 0 {
|
||||
res.AddOption(gocoap.Observe, 1)
|
||||
o := coap.NewObserver()
|
||||
if err := svc.Subscribe(chanID, subtopic, obsID, o); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Failed to subscribe to NATS subject: %s", err))
|
||||
res.Code = gocoap.InternalServerError
|
||||
return res
|
||||
}
|
||||
|
||||
go handleMessage(conn, addr, o, msg)
|
||||
go ping(svc, obsID, conn, addr, o, msg)
|
||||
go cancel(o)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func cancel(observer *coap.Observer) {
|
||||
<-observer.Cancel
|
||||
close(observer.Messages)
|
||||
observer.StoreExpired(true)
|
||||
}
|
||||
|
||||
func handleMessage(conn *net.UDPConn, addr *net.UDPAddr, o *coap.Observer, msg *gocoap.Message) {
|
||||
notifyMsg := *msg
|
||||
notifyMsg.Type = gocoap.NonConfirmable
|
||||
notifyMsg.Code = gocoap.Content
|
||||
notifyMsg.RemoveOption(gocoap.URIQuery)
|
||||
for {
|
||||
msg, ok := <-o.Messages
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
payload, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
logger.Warn(fmt.Sprintf("Failed to parse received message: %s", err))
|
||||
continue
|
||||
}
|
||||
|
||||
notifyMsg.Payload = payload
|
||||
notifyMsg.MessageID = o.LoadMessageID()
|
||||
buff := new(bytes.Buffer)
|
||||
observe := uint64(notifyMsg.MessageID)
|
||||
if err := binary.Write(buff, binary.BigEndian, observe); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Failed to generate Observe option value: %s", err))
|
||||
continue
|
||||
}
|
||||
|
||||
observeVal := buff.Bytes()
|
||||
notifyMsg.SetOption(gocoap.Observe, observeVal[len(observeVal)-3:])
|
||||
|
||||
if err := gocoap.Transmit(conn, addr, notifyMsg); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Failed to send message to observer: %s", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ping(svc coap.Service, obsID string, conn *net.UDPConn, addr *net.UDPAddr, o *coap.Observer, msg *gocoap.Message) {
|
||||
pingMsg := *msg
|
||||
pingMsg.Payload = []byte{}
|
||||
pingMsg.Type = gocoap.Confirmable
|
||||
pingMsg.RemoveOption(gocoap.URIQuery)
|
||||
// According to RFC (https://tools.ietf.org/html/rfc7641#page-18), CON message must be sent at least every
|
||||
// 24 hours. Deafault value of pingPeriod is 12.
|
||||
t := time.NewTicker(pingPeriod * time.Hour)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-t.C:
|
||||
if !ok || o.LoadExpired() {
|
||||
return
|
||||
}
|
||||
|
||||
o.StoreExpired(true)
|
||||
timeout := float64(coap.AckTimeout)
|
||||
logger.Info(fmt.Sprintf("Ping client %s.", obsID))
|
||||
for i := 0; i < coap.MaxRetransmit; i++ {
|
||||
pingMsg.MessageID = o.LoadMessageID()
|
||||
gocoap.Transmit(conn, addr, pingMsg)
|
||||
time.Sleep(time.Duration(timeout * coap.AckRandomFactor))
|
||||
if !o.LoadExpired() {
|
||||
break
|
||||
}
|
||||
timeout = 2 * timeout
|
||||
}
|
||||
|
||||
if o.LoadExpired() {
|
||||
svc.Unsubscribe(obsID)
|
||||
return
|
||||
}
|
||||
case <-o.Cancel:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package api
|
||||
|
||||
import "strings"
|
||||
|
||||
func authKey(opt interface{}) (string, error) {
|
||||
val, ok := opt.(string)
|
||||
if !ok {
|
||||
return "", errBadRequest
|
||||
}
|
||||
|
||||
arr := strings.Split(val, "=")
|
||||
if len(arr) != 2 || strings.ToLower(arr[0]) != "authorization" {
|
||||
return "", errBadOption
|
||||
}
|
||||
|
||||
return arr[1], nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Package nats contains NATS message publisher implementation.
|
||||
package nats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const prefix = "channel"
|
||||
|
||||
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
|
||||
|
||||
type natsPublisher struct {
|
||||
nc *broker.Conn
|
||||
}
|
||||
|
||||
// New instantiates NATS message publisher.
|
||||
func New(nc *broker.Conn) coap.Broker {
|
||||
return &natsPublisher{nc}
|
||||
}
|
||||
|
||||
func (pubsub *natsPublisher) fmtSubject(chanID, subtopic string) string {
|
||||
subject := fmt.Sprintf("%s.%s", prefix, chanID)
|
||||
if subtopic != "" {
|
||||
subject = fmt.Sprintf("%s.%s", subject, subtopic)
|
||||
}
|
||||
return subject
|
||||
}
|
||||
|
||||
func (pubsub *natsPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
data, err := proto.Marshal(&msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subject := pubsub.fmtSubject(msg.Channel, msg.Subtopic)
|
||||
return pubsub.nc.Publish(subject, data)
|
||||
}
|
||||
|
||||
func (pubsub *natsPublisher) Subscribe(chanID, subtopic, obsID string, observer *coap.Observer) error {
|
||||
subject := pubsub.fmtSubject(chanID, subtopic)
|
||||
sub, err := pubsub.nc.Subscribe(subject, func(msg *broker.Msg) {
|
||||
if msg == nil {
|
||||
return
|
||||
}
|
||||
var rawMsg mainflux.RawMessage
|
||||
if err := proto.Unmarshal(msg.Data, &rawMsg); err != nil {
|
||||
return
|
||||
}
|
||||
observer.Messages <- rawMsg
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-observer.Cancel
|
||||
sub.Unsubscribe()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// Copyright (c) 2018
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
package coap
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
// Observer is used to handle CoAP subscription.
|
||||
type Observer struct {
|
||||
// Expired flag is used to mark that ticker sent a
|
||||
// CON message, but response is not received yet.
|
||||
// The flag changes its value once ACK message is
|
||||
// received from the client. If Expired is true
|
||||
// when ticker is triggered, Observer should be canceled
|
||||
// and removed from the Service map.
|
||||
expired bool
|
||||
|
||||
// Message ID for notification messages.
|
||||
msgID uint16
|
||||
|
||||
expiredLock, msgIDLock sync.Mutex
|
||||
|
||||
// Messages is used to receive messages from NATS.
|
||||
Messages chan mainflux.RawMessage
|
||||
|
||||
// Cancel channel is used to cancel observing resource.
|
||||
// Cancel channel should not be used to send or receive any
|
||||
// data, it's purpose is to be closed once Observer canceled.
|
||||
Cancel chan bool
|
||||
}
|
||||
|
||||
// NewObserver instantiates a new Observer.
|
||||
func NewObserver() *Observer {
|
||||
return &Observer{
|
||||
Messages: make(chan mainflux.RawMessage),
|
||||
Cancel: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadExpired reads Expired flag in thread-safe way.
|
||||
func (o *Observer) LoadExpired() bool {
|
||||
o.expiredLock.Lock()
|
||||
defer o.expiredLock.Unlock()
|
||||
|
||||
return o.expired
|
||||
}
|
||||
|
||||
// StoreExpired stores Expired flag in thread-safe way.
|
||||
func (o *Observer) StoreExpired(val bool) {
|
||||
o.expiredLock.Lock()
|
||||
defer o.expiredLock.Unlock()
|
||||
|
||||
o.expired = val
|
||||
}
|
||||
|
||||
// LoadMessageID reads MessageID and increments
|
||||
// its value in thread-safe way.
|
||||
func (o *Observer) LoadMessageID() uint16 {
|
||||
o.msgIDLock.Lock()
|
||||
defer o.msgIDLock.Unlock()
|
||||
|
||||
o.msgID++
|
||||
return o.msgID
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"project": {
|
||||
"name": "mainflux-ui"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"favicon.png"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.app.json",
|
||||
"testTsconfig": "tsconfig.spec.json",
|
||||
"prefix": "app",
|
||||
"styles": [
|
||||
"styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": [
|
||||
{
|
||||
"project": "src/tsconfig.app.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
},
|
||||
{
|
||||
"project": "src/tsconfig.spec.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
},
|
||||
{
|
||||
"project": "e2e/tsconfig.e2e.json",
|
||||
"exclude": "**/node_modules/**"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "scss",
|
||||
"component": {}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
# Editor configuration, see http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
@@ -1,58 +0,0 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/bower_components
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
/.vscode
|
||||
/out-tsc
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage/*
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# e2e
|
||||
/e2e/*.js
|
||||
/e2e/*.map
|
||||
|
||||
#System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
#json-server database
|
||||
db.json
|
||||
db.*
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -1,73 +0,0 @@
|
||||
# Mainflux UI Dashboad
|
||||
|
||||
Mainflux UI, dashboard for [Mainflux](https://github.com/mainflux/mainflux) Industrial IoT Messaging and Device Management Server.
|
||||
|
||||
> **N.B.** Mainflux UI service is WIP and not suitable for deployment at this moment. You are welcome to contribute and improve it.
|
||||
> ## Development
|
||||
>- Follow angular-cli [documentation](https://github.com/angular/angular-cli)
|
||||
>- Follow [official angular style guide](https://angular.io/styleguide)
|
||||
|
||||
## Requirements
|
||||
|
||||
You'll need the following software installed to get started.
|
||||
|
||||
- [Node](https://nodejs.org/en/) 6 or higher, we recommend current LTS version, together with NPM 3 or higher.
|
||||
- [Angular-cli](https://github.com/angular/angular-cli) Newest version with Webpack integration
|
||||
- - Depending on how Node is configured on your machine, you may need to run installation command with `sudo`
|
||||
- [Git](http://git-scm.com/downloads): Use the installer for your OS.
|
||||
- Windows users can also try [Git for Windows](http://git-for-windows.github.io/).
|
||||
- For local Development with [Mainflux composition](https://github.com/mainflux/mainflux) running locally, [Chrome extension for Cross origin](https://chrome.google.com/webstore/detail/allow-control-allow-origi/nlfbmbojpeacfghkpbjhddihlkkiljbi?utm_source=chrome-app-launcher-info-dialog) is required. Because composition is running on different port then our Angular app, we have cross origin.
|
||||
|
||||
## Configuration
|
||||
|
||||
Change into the directory.
|
||||
|
||||
```bash
|
||||
cd dashflux
|
||||
```
|
||||
|
||||
Install the dependencies. If you're running Mac OS or Linux, you may need to run `sudo npm install` instead, depending on how your machine is configured.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Set appropriate endpoint URLs in **environment.ts** (for local development will probably be 0.0.0.0:<_port_>) or **environment.prod.ts** for production.
|
||||
|
||||
To start the server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
This will run and assemble our app.
|
||||
**Now go to `localhost:4200` in your browser to see it in action.**
|
||||
|
||||
## Deployment
|
||||
|
||||
Dashflux is distributed as Docker container. We use nginx to serve dashflux from docker container, supporting environments using docker multi-stage builds.
|
||||
Dashflux docker image is available on [Dockerhub mainflux/dashflux](https://hub.docker.com/r/mainflux/dashflux/)
|
||||
|
||||
If you want to build image locally, you can build image using the **development** environment:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile -t dashflux:dev --build-arg env=dev .
|
||||
```
|
||||
|
||||
Build image using the **production** environment:
|
||||
|
||||
```bash
|
||||
docker build -t dashflux:prod -f ./docker/Dockerfile .
|
||||
```
|
||||
|
||||
**Note:** before running *docker build* command, please make sure appropriate endpoint URLs in *environment.ts* or *environment.prod.ts* are set up.
|
||||
|
||||
You can test image running
|
||||
|
||||
```bash
|
||||
docker run -p 80:80 dashflux:dev
|
||||
```
|
||||
|
||||
This will run dashflux in docker container.
|
||||
|
||||
Now go to `http://localhost` in your browser to see it in action.
|
||||
@@ -1 +0,0 @@
|
||||
node_modules
|
||||
@@ -1,13 +0,0 @@
|
||||
# Stage 0, based on Node.js, to build and compile Angular
|
||||
FROM node:9.5 as node
|
||||
WORKDIR /app
|
||||
COPY package.json /app/
|
||||
RUN npm install
|
||||
COPY ./ /app/
|
||||
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-alpine
|
||||
COPY --from=node /app/dist/ /usr/share/nginx/html
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
@@ -1,14 +0,0 @@
|
||||
import { AppPage } from './app.po';
|
||||
|
||||
describe('mainflux-ui App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getParagraphText()).toEqual('Welcome to app!');
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getParagraphText() {
|
||||
return element(by.css('app-root h1')).getText();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"baseUrl": "./",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular/cli'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular/cli/plugins/karma')
|
||||
],
|
||||
client:{
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
reports: [ 'html', 'lcovonly' ],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
angularCli: {
|
||||
environment: 'dev'
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['ChromeHeadless'],
|
||||
singleRun: false
|
||||
});
|
||||
};
|
||||
Generated
-10394
File diff suppressed because it is too large
Load Diff
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"name": "mainflux-ui",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache License, version 2.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy-config.json",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^5.2.4",
|
||||
"@angular/common": "^5.2.4",
|
||||
"@angular/compiler": "^5.2.4",
|
||||
"@angular/core": "^5.2.4",
|
||||
"@angular/flex-layout": "2.0.0-beta.10-4905443",
|
||||
"@angular/forms": "^5.2.4",
|
||||
"@angular/http": "^5.2.4",
|
||||
"@angular/platform-browser": "^5.2.4",
|
||||
"@angular/platform-browser-dynamic": "^5.2.4",
|
||||
"@angular/router": "^5.2.4",
|
||||
"core-js": "^2.4.1",
|
||||
"mobx": "^3.5.1",
|
||||
"mobx-angular": "^2.1.1",
|
||||
"ngx-auth": "^2.2.0",
|
||||
"rxjs": "^5.5.2",
|
||||
"zone.js": "^0.8.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cdk": "^5.2.0",
|
||||
"@angular/cli": "^1.7.3",
|
||||
"@angular/compiler-cli": "^5.2.4",
|
||||
"@angular/language-service": "^5.2.4",
|
||||
"@angular/material": "^5.2.0",
|
||||
"@types/jasmine": "~2.5.53",
|
||||
"@types/jasminewd2": "~2.0.2",
|
||||
"@types/node": "^6.0.100",
|
||||
"codelyzer": "~3.2.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"jasmine-core": "~2.5.2",
|
||||
"jasmine-spec-reporter": "~4.1.0",
|
||||
"karma": "~1.7.0",
|
||||
"karma-chrome-launcher": "~2.1.1",
|
||||
"karma-cli": "~1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"karma-jasmine": "~1.1.0",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"karma-phantomjs-launcher": "^1.0.4",
|
||||
"protractor": "~5.1.2",
|
||||
"ts-node": "~3.2.0",
|
||||
"tslint": "~5.7.0",
|
||||
"typescript": "~2.4.2"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./e2e/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: 'e2e/tsconfig.e2e.json'
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"/api/*": {
|
||||
"target": "http://localhost:8180",
|
||||
"secure":false,
|
||||
"pathRewrite": {
|
||||
"^/api": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* 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';
|
||||
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 { ThingsComponent } from './components/things/things.component';
|
||||
|
||||
const routes: Route[] = [
|
||||
{ path: '', redirectTo: 'things', pathMatch: 'full'},
|
||||
{ path: 'login', component: LoginComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'signup', component: SignupComponent, canActivate: [PublicGuard]},
|
||||
{ path: 'things', component: ThingsComponent, canActivate: [ProtectedGuard]},
|
||||
{ path: 'channels', component: ChannelsComponent, canActivate: [ProtectedGuard]}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule.forRoot(routes)
|
||||
],
|
||||
exports: [
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@@ -1,22 +0,0 @@
|
||||
<!--
|
||||
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>
|
||||
</div>
|
||||
<mat-toolbar color="primary" class="mat-elevation-z6">
|
||||
<h1 fxFlex>Mainflux</h1>
|
||||
<ng-container *ngIf="authStore.isAuthenticated">
|
||||
<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>
|
||||
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
import { AppComponent } from './app.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 { ThingsService } from './core/services/things/things.service';
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
it('should create the app', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
import { UiStore } from './core/store/ui.store';
|
||||
import { AuthStore } from './core/store/auth.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(
|
||||
private snackBar: MatSnackBar,
|
||||
public uiStore: UiStore,
|
||||
public authStore: AuthStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
reaction(() => this.authStore.authError, (authError) => {
|
||||
if (authError) {
|
||||
this.snackBar.open(authError, '', {
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authStore.logout();
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* 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';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MobxAngularModule } from 'mobx-angular';
|
||||
import { AUTH_SERVICE, AuthModule, PROTECTED_FALLBACK_PAGE_URI, PUBLIC_FALLBACK_PAGE_URI } from 'ngx-auth';
|
||||
|
||||
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 { ChannelDialogComponent } from './components/channels/channel-dialog/channel-dialog.component';
|
||||
import { ChannelsComponent } from './components/channels/channels.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 { ThingsService } from './core/services/things/things.service';
|
||||
import { MockAuthService } from './core/services/mock-auth.service';
|
||||
import { MockChannelsService } from './core/services/mock-channels.service';
|
||||
import { MockThingsService } from './core/services/mock-things.service';
|
||||
import { ChannelsStore } from './core/store/channels.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';
|
||||
|
||||
export function factory(authenticationService: AuthenticationService) {
|
||||
return authenticationService;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
ThingsComponent,
|
||||
ChannelsComponent,
|
||||
SignupComponent,
|
||||
LoginComponent,
|
||||
ThingDialogComponent,
|
||||
ConfirmationDialogComponent,
|
||||
ChannelDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
AuthModule,
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
MaterialModule,
|
||||
FlexLayoutModule,
|
||||
ReactiveFormsModule,
|
||||
MobxAngularModule,
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
ThingsStore,
|
||||
ChannelsStore,
|
||||
AuthStore,
|
||||
MockAuthService,
|
||||
MockThingsService,
|
||||
MockChannelsService,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
TokenStorage,
|
||||
AuthenticationService,
|
||||
{ provide: PROTECTED_FALLBACK_PAGE_URI, useValue: '/' },
|
||||
{ provide: PUBLIC_FALLBACK_PAGE_URI, useValue: '/login' },
|
||||
{
|
||||
provide: AUTH_SERVICE,
|
||||
deps: [AuthenticationService],
|
||||
useFactory: factory
|
||||
},
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: UnauthorizedInterceptor, multi: true },
|
||||
{ provide: APP_BASE_HREF, useValue: '/app/' }
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
entryComponents: [
|
||||
ThingDialogComponent,
|
||||
ChannelDialogComponent,
|
||||
ConfirmationDialogComponent
|
||||
]
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -1,35 +0,0 @@
|
||||
<!--
|
||||
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>
|
||||
<mat-card-title>Login</mat-card-title>
|
||||
<mat-card-content class="loginCard" fxLayout="column" fxLayoutAlign="space-evenly">
|
||||
<mat-form-field fxFlex="40%">
|
||||
<input type="text" class="emailInput" required matInput placeholder="Email" formControlName="email"/>
|
||||
<mat-error *ngIf="loginForm.get('email').errors?.required">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="loginForm.get('email').errors?.email">
|
||||
Email must be valid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%">
|
||||
<input type="password" class="passwordInput" required matInput placeholder="Password" formControlName="password" />
|
||||
<mat-error *ngIf="loginForm.get('password').errors?.required">
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions fxLayout="column">
|
||||
<button type="submit" [disabled]="loginForm.invalid" mat-raised-button color="primary" class="loginButton">Login</button>
|
||||
<button mat-button (click)="signup()" class="signupButton">Sign Up with Email</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
::ng-deep mat-card-content.loginCard {
|
||||
height: 200px;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* 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';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
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 { LoginComponent } from './login.component';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call the store goToSignup when clicked on Sign Up with Email', inject([UiStore], (store: UiStore) => {
|
||||
const signupButton = fixture.debugElement.nativeElement.querySelector('.signupButton');
|
||||
const signupSpy = spyOn(store, 'goToSignup').and.stub();
|
||||
|
||||
signupButton.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(signupSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
loginForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private uiStore: UiStore,
|
||||
private authStore: AuthStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.loginForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required]],
|
||||
});
|
||||
}
|
||||
|
||||
login() {
|
||||
this.authStore.login(this.getUserDataFromForm());
|
||||
}
|
||||
|
||||
signup() {
|
||||
this.uiStore.goToSignup();
|
||||
}
|
||||
|
||||
getUserDataFromForm() {
|
||||
return {
|
||||
email: this.loginForm.get('email').value,
|
||||
password: this.loginForm.get('password').value
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<!--
|
||||
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>
|
||||
<mat-card-title>Signup</mat-card-title>
|
||||
<mat-card-content class="loginCard" fxLayout="column" fxLayoutAlign="space-evenly">
|
||||
<mat-form-field fxFlex="40%">
|
||||
<input type="text" required matInput placeholder="Email" formControlName="email"/>
|
||||
<mat-error *ngIf="signupForm.get('email').errors?.required">
|
||||
Email is required
|
||||
</mat-error>
|
||||
<mat-error *ngIf="signupForm.get('email').errors?.email">
|
||||
Email must be valid
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%" formGroupName="passwords">
|
||||
<input type="password" required matInput placeholder="Password" formControlName="password" />
|
||||
<mat-error *ngIf="signupForm.get('passwords.password').errors?.required">
|
||||
Password is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex="30%" formGroupName="passwords">
|
||||
<input type="password" required matInput placeholder="Repeat password" formControlName="repeatPassword" />
|
||||
<mat-error *ngIf="signupForm.get('passwords.repeatPassword').errors?.required">
|
||||
Repeat the password
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions fxLayout="column">
|
||||
<button type="submit" [disabled]="signupForm.invalid" mat-raised-button color="primary" class="large-button">Signup</button>
|
||||
<a mat-button (click)="login()">Login</a>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
::ng-deep mat-card-content.loginCard {
|
||||
height: 200px;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* 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';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
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 { SignupComponent } from './signup.component';
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
|
||||
describe('SignupComponent', () => {
|
||||
let component: SignupComponent;
|
||||
let fixture: ComponentFixture<SignupComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SignupComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
UiStore,
|
||||
AuthStore,
|
||||
AuthenticationService,
|
||||
TokenStorage
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SignupComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { AuthStore } from '../../../core/store/auth.store';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-signup',
|
||||
templateUrl: './signup.component.html',
|
||||
styleUrls: ['./signup.component.scss']
|
||||
})
|
||||
export class SignupComponent implements OnInit {
|
||||
signupForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private uiStore: UiStore,
|
||||
private authStore: AuthStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.signupForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
passwords: this.fb.group({
|
||||
password: ['', [Validators.required]],
|
||||
repeatPassword: ['', [Validators.required]]
|
||||
}, { validator: this.comparePasswords })
|
||||
});
|
||||
}
|
||||
|
||||
comparePasswords(c: AbstractControl): { [key: string]: boolean } {
|
||||
const pass = c.get('password');
|
||||
const repeatPassword = c.get('repeatPassword');
|
||||
|
||||
if (pass.value !== repeatPassword.value) {
|
||||
return { 'match': true };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
signup() {
|
||||
this.authStore.signup(this.getUserDataFromForm());
|
||||
}
|
||||
|
||||
login() {
|
||||
this.uiStore.goToLogin();
|
||||
}
|
||||
|
||||
getUserDataFromForm() {
|
||||
return {
|
||||
email: this.signupForm.get('email').value,
|
||||
password: this.signupForm.get('passwords.password').value
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user