mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 23:32:14 +00:00
Compare commits
69 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 |
+1
-1
@@ -1,7 +1,7 @@
|
||||
.git
|
||||
.github
|
||||
build
|
||||
dashflux
|
||||
ui
|
||||
docker
|
||||
docs
|
||||
k8s
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
build
|
||||
|
||||
site/
|
||||
|
||||
|
||||
+73
-6
@@ -11,6 +11,73 @@ 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
|
||||
|
||||
@@ -49,7 +116,7 @@ https://github.com/mainflux/mainflux/milestone/8?closed=1
|
||||
|
||||
|
||||
## 0.6.0 - 26. OCT 2018.
|
||||
### Features
|
||||
### Features
|
||||
|
||||
- Added Go SDK (#357)
|
||||
- Updated NATS version (#412)
|
||||
@@ -73,7 +140,7 @@ https://github.com/mainflux/mainflux/milestone/8?closed=1
|
||||
- Cassandra Reader (#313)
|
||||
- MongoDB Reader (#344)
|
||||
- MQTT Persistance via Redis (#328)
|
||||
- CLI integrated into monorepo (#216)
|
||||
- CLI integrated into monorepo (#216)
|
||||
- Normalizer logging (#333)
|
||||
- WS swagger doc (#337)
|
||||
- Payload renamed to Metadata (#343)
|
||||
@@ -90,10 +157,10 @@ 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 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 )
|
||||
* Replaced JWT with plain string tokens in things service (#268 )
|
||||
* Emit non-SenML messages (#239 )
|
||||
* Support for Grafana (#296)
|
||||
* Added WS Load test (#299 )
|
||||
@@ -125,7 +192,7 @@ https://github.com/mainflux/mainflux/milestone/6?closed=1
|
||||
## 0.2.0 - 18. APR 2018
|
||||
- Protobuf message serialization (#192)
|
||||
- Websocket API for exchanging messages (#188)
|
||||
- Channel & client retrieval paging (#227)
|
||||
- Channel & client retrieval paging (#227)
|
||||
- Service instrumentation (#213)
|
||||
- `go-kit` based JSON logger (#212)
|
||||
- Project documentation (#218, #220)
|
||||
@@ -137,4 +204,4 @@ https://github.com/mainflux/mainflux/milestone/6?closed=1
|
||||
|
||||
- Fixed go lint warnings (#189)
|
||||
- Compose failing startup (#185)
|
||||
- Added missing service startup messages (#190)
|
||||
- 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
+252
-79
@@ -3,122 +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 = ["."]
|
||||
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"
|
||||
"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"
|
||||
"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",
|
||||
@@ -127,18 +156,22 @@
|
||||
"metrics/internal/lv",
|
||||
"metrics/prometheus",
|
||||
"transport/grpc",
|
||||
"transport/http"
|
||||
"transport/http",
|
||||
]
|
||||
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 = [
|
||||
".",
|
||||
@@ -148,214 +181,248 @@
|
||||
"internal/pool",
|
||||
"internal/proto",
|
||||
"internal/singleflight",
|
||||
"internal/util"
|
||||
"internal/util",
|
||||
]
|
||||
revision = "f3bba01df2026fc865f7782948845db9cf44cf23"
|
||||
version = "v6.14.1"
|
||||
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",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "68ae1e384be4d7cd7df0f5b77f331759d806308e"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bbadccf3d3317ea03c0dac0b45b673b4b397c8f91a1d2eff550a3c51c4ad770e"
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["proto"]
|
||||
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",
|
||||
]
|
||||
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 = ["."]
|
||||
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",
|
||||
]
|
||||
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",
|
||||
]
|
||||
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",
|
||||
@@ -381,127 +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/internal",
|
||||
"prometheus/promhttp"
|
||||
"prometheus/promhttp",
|
||||
]
|
||||
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",
|
||||
]
|
||||
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 = ["."]
|
||||
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",
|
||||
]
|
||||
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",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "4d3f4d9ffa16a13f451c3b2999e9c49e9750bf06"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:70e155783b8dfd4f7697243384e2f361930ff0751459580769ce44d511f38b05"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
@@ -513,26 +628,32 @@
|
||||
"internal/timeseries",
|
||||
"proxy",
|
||||
"trace",
|
||||
"websocket"
|
||||
"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",
|
||||
@@ -548,18 +669,22 @@
|
||||
"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 = [
|
||||
".",
|
||||
@@ -587,32 +712,80 @@
|
||||
"resolver/passthrough",
|
||||
"stats",
|
||||
"status",
|
||||
"tap"
|
||||
"tap",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "2e463a05d100327ca47ac218281906921038fd95"
|
||||
version = "v1.16.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fa9a7c0ef59217bd22f32eb7cc027894d73f340a5633258cc079dec025db6a7f"
|
||||
name = "gopkg.in/gorp.v1"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "c87af80f3cc5036b55b83d77171e156791085e2e"
|
||||
version = "v1.7.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a"
|
||||
name = "gopkg.in/inf.v0"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf"
|
||||
version = "v0.9.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:de4bc452b049bde272d99e944aabcf218dc22df0a478a42a7107932f82430ae2"
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "9f1141b78507d23603f6be530587664ebb63a8d2"
|
||||
version = "v3.3.2"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "96f3fcd7c3539e021230f0a500f0c3281f6ad6b06e455a9e9585b89f932f184e"
|
||||
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
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/hokaccha/go-prettyjson"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/lib/pq"
|
||||
branch = "master"
|
||||
@@ -102,6 +106,10 @@
|
||||
name = "google.golang.org/grpc"
|
||||
version = "1.16.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-redis/redis"
|
||||
version = "v6.15.0"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = 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.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
## Copyright (c) 2015-2019
|
||||
## Mainflux
|
||||
##
|
||||
## SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
BUILD_DIR = build
|
||||
SERVICES = users things http normalizer ws coap lora 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
|
||||
@@ -19,7 +24,7 @@ endef
|
||||
|
||||
all: $(SERVICES) mqtt
|
||||
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release mqtt
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release mqtt ui
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
@@ -46,7 +51,7 @@ 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 --gofast_out=plugins=grpc:. *.proto
|
||||
@@ -57,15 +62,23 @@ $(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
|
||||
|
||||
@@ -73,7 +86,7 @@ define docker_push
|
||||
for svc in $(SERVICES); do \
|
||||
docker push mainflux/$$svc:$(1); \
|
||||
done
|
||||
docker push mainflux/dashflux:$(1)
|
||||
docker push mainflux/ui:$(1)
|
||||
docker push mainflux/mqtt:$(1)
|
||||
endef
|
||||
|
||||
@@ -90,7 +103,7 @@ release:
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
|
||||
done
|
||||
docker tag mainflux/dashflux mainflux/dashflux:$(version)
|
||||
docker tag mainflux/ui mainflux/ui:$(version)
|
||||
docker tag mainflux/mqtt mainflux/mqtt:$(version)
|
||||
$(call docker_push,$(version))
|
||||
|
||||
@@ -100,6 +113,9 @@ rundev:
|
||||
run:
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
|
||||
runui:
|
||||
$(MAKE) -C ui run
|
||||
|
||||
runlora:
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
docker-compose -f docker/addons/influxdb-writer/docker-compose.yml up -d
|
||||
|
||||
@@ -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
|
||||
+45
-24
@@ -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,17 +24,27 @@ 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 update '{"id":"<thing_id>", "name":"myNewName"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Remove Thing
|
||||
```
|
||||
mainflux-cli things delete <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of provisioned Things
|
||||
```
|
||||
mainflux-cli things get all --offset=1 --limit=5 <user_auth_token>
|
||||
```
|
||||
@@ -44,44 +54,55 @@ mainflux-cli things get all --offset=1 --limit=5 <user_auth_token>
|
||||
mainflux-cli things get <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Remove Thing
|
||||
#### Create Channel
|
||||
```
|
||||
mainflux-cli things delete <thing_id> <user_auth_token>
|
||||
mainflux-cli channels create '{"name":"myChannel"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Provision Channel
|
||||
```
|
||||
mainflux-cli channels create '{"name":"nyChannel"}' <user_auth_token>
|
||||
#### Update Channel
|
||||
```
|
||||
mainflux-cli channels update '{"id":"<channel_id>","name":"myNewName"}' <user_auth_token>
|
||||
|
||||
#### Retrieve All Channels
|
||||
```
|
||||
mainflux-cli channels get all --offset=1 --limit=5 <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrievie Channel By ID
|
||||
```
|
||||
mainflux-cli channels get <channel_id> <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>
|
||||
```
|
||||
|
||||
+27
-8
@@ -37,12 +37,12 @@ var cmdChannels = []cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
flush(id)
|
||||
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 {
|
||||
@@ -57,7 +57,7 @@ var cmdChannels = []cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
flush(l)
|
||||
logJSON(l)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ var cmdChannels = []cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
flush(c)
|
||||
logJSON(c)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
@@ -75,7 +75,7 @@ var cmdChannels = []cobra.Command{
|
||||
Short: "update <JSON_string> <user_auth_token>",
|
||||
Long: `Updates channel record`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
@@ -112,16 +112,35 @@ var cmdChannels = []cobra.Command{
|
||||
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]")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+26
-4
@@ -14,7 +14,7 @@ const contentTypeSenml = "application/senml+json"
|
||||
var cmdMessages = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "send",
|
||||
Short: "send <channel_id> <JSON_string> <thing_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 {
|
||||
@@ -30,14 +30,36 @@ var cmdMessages = []cobra.Command{
|
||||
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 {
|
||||
|
||||
+3
-6
@@ -101,7 +101,7 @@ var cmdProvision = []cobra.Command{
|
||||
things = append(things, m)
|
||||
}
|
||||
|
||||
flush(things)
|
||||
logJSON(things)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
@@ -147,7 +147,7 @@ var cmdProvision = []cobra.Command{
|
||||
channels = append(channels, c)
|
||||
}
|
||||
|
||||
flush(channels)
|
||||
logJSON(channels)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
@@ -230,10 +230,7 @@ var cmdProvision = []cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
flush(user)
|
||||
flush(ut)
|
||||
flush(things)
|
||||
flush(channels)
|
||||
logJSON(user, ut, things, channels)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+27
-8
@@ -39,12 +39,12 @@ var cmdThings = []cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
flush(id)
|
||||
logCreated(id)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "get",
|
||||
Short: "get all/<thing_id> <user_auth_token>",
|
||||
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 {
|
||||
@@ -58,7 +58,7 @@ var cmdThings = []cobra.Command{
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
flush(l)
|
||||
logJSON(l)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ var cmdThings = []cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
flush(t)
|
||||
logJSON(t)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
@@ -94,7 +94,7 @@ var cmdThings = []cobra.Command{
|
||||
Short: "update <JSON_string> <user_auth_token>",
|
||||
Long: `Update thing record`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
@@ -149,16 +149,35 @@ var cmdThings = []cobra.Command{
|
||||
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]")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -55,7 +55,7 @@ var cmdUsers = []cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
flush(token)
|
||||
logCreated(token)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -64,10 +64,10 @@ var cmdUsers = []cobra.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]")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+25
-6
@@ -8,10 +8,11 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/fatih/color"
|
||||
prettyjson "github.com/hokaccha/go-prettyjson"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -21,18 +22,36 @@ var (
|
||||
Offset uint
|
||||
)
|
||||
|
||||
func flush(i interface{}) {
|
||||
fmt.Printf("%s", color.BlueString(spew.Sdump(i)))
|
||||
func logJSON(iList ...interface{}) {
|
||||
for _, i := range iList {
|
||||
m, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
pj, err := prettyjson.Format(m)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s\n\n", string(pj))
|
||||
}
|
||||
}
|
||||
|
||||
func logUsage(u string) {
|
||||
fmt.Printf(color.YellowString("Usage: %s\n"), u)
|
||||
fmt.Printf(color.YellowString("\nusage: %s\n\n"), u)
|
||||
}
|
||||
|
||||
func logError(err error) {
|
||||
fmt.Printf("%s\n", color.RedString(err.Error()))
|
||||
fmt.Printf("\n%s\n\n", color.RedString(err.Error()))
|
||||
}
|
||||
|
||||
func logOK() {
|
||||
fmt.Printf("%s\n", color.GreenString("OK"))
|
||||
fmt.Printf("\n%s\n\n", color.BlueString("ok"))
|
||||
}
|
||||
|
||||
func logCreated(e string) {
|
||||
fmt.Printf(color.BlueString("\ncreated: %s\n\n"), e)
|
||||
}
|
||||
|
||||
+3
-3
@@ -13,8 +13,8 @@ import "github.com/spf13/cobra"
|
||||
func NewVersionCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Get version of Mainflux Things Service",
|
||||
Long: `Mainflux server health check`,
|
||||
Short: "Mainflux services version",
|
||||
Long: `Mainflux services version: get version of Mainflux Things Service`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
v, err := sdk.Version()
|
||||
if err != nil {
|
||||
@@ -22,7 +22,7 @@ func NewVersionCmd() *cobra.Command {
|
||||
return
|
||||
}
|
||||
|
||||
flush(v)
|
||||
logJSON(v)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -19,6 +19,8 @@ func main() {
|
||||
msgContentType := string(sdk.CTJSONSenML)
|
||||
sdkConf := sdk.Config{
|
||||
BaseURL: "http://localhost",
|
||||
ReaderURL: "http://localhost:8905",
|
||||
ReaderPrefix: "",
|
||||
UsersPrefix: "",
|
||||
ThingsPrefix: "",
|
||||
HTTPAdapterPrefix: "http",
|
||||
|
||||
+4
-2
@@ -38,7 +38,7 @@ const (
|
||||
defRouteMapDB = "0"
|
||||
|
||||
envHTTPPort = "MF_LORA_ADAPTER_HTTP_PORT"
|
||||
envLoraMsgURL = "MF_LORA_ADAPTER_LORA_MESSAGE_URL"
|
||||
envLoraMsgURL = "MF_LORA_ADAPTER_MESSAGES_URL"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_LORA_ADAPTER_LOG_LEVEL"
|
||||
envESURL = "MF_THINGS_ES_URL"
|
||||
@@ -203,7 +203,9 @@ func subscribeToLoRaBroker(svc lora.Service, mc mqtt.Client, logger logger.Logge
|
||||
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")
|
||||
eventStore.Subscribe("mainflux.things")
|
||||
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 {
|
||||
|
||||
@@ -167,5 +167,5 @@ func newService(db *mongo.Database, logger logger.Logger) readers.MessageReposit
|
||||
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"))
|
||||
}
|
||||
|
||||
+82
-60
@@ -37,55 +37,65 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defLogLevel = "error"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "things"
|
||||
defDBSSLMode = "disable"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defCacheURL = "localhost:6379"
|
||||
defCachePass = ""
|
||||
defCacheDB = "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"
|
||||
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"
|
||||
envHTTPPort = "MF_THINGS_HTTP_PORT"
|
||||
envGRPCPort = "MF_THINGS_GRPC_PORT"
|
||||
envUsersURL = "MF_USERS_URL"
|
||||
envServerCert = "MF_THINGS_SERVER_CERT"
|
||||
envServerKey = "MF_THINGS_SERVER_KEY"
|
||||
defLogLevel = "error"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "things"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defCacheURL = "localhost:6379"
|
||||
defCachePass = ""
|
||||
defCacheDB = "0"
|
||||
defESURL = "localhost:6379"
|
||||
defESPass = ""
|
||||
defESDB = "0"
|
||||
defHTTPPort = "8180"
|
||||
defGRPCPort = "8181"
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defUsersURL = "localhost:8181"
|
||||
envLogLevel = "MF_THINGS_LOG_LEVEL"
|
||||
envDBHost = "MF_THINGS_DB_HOST"
|
||||
envDBPort = "MF_THINGS_DB_PORT"
|
||||
envDBUser = "MF_THINGS_DB_USER"
|
||||
envDBPass = "MF_THINGS_DB_PASS"
|
||||
envDBName = "MF_THINGS_DB"
|
||||
envDBSSLMode = "MF_THINGS_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_THINGS_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_THINGS_DB_SSL_KEY"
|
||||
envDBSSLRootCert = "MF_THINGS_DB_SSL_ROOT_CERT"
|
||||
envClientTLS = "MF_THINGS_CLIENT_TLS"
|
||||
envCACerts = "MF_THINGS_CA_CERTS"
|
||||
envCacheURL = "MF_THINGS_CACHE_URL"
|
||||
envCachePass = "MF_THINGS_CACHE_PASS"
|
||||
envCacheDB = "MF_THINGS_CACHE_DB"
|
||||
envESURL = "MF_THINGS_ES_URL"
|
||||
envESPass = "MF_THINGS_ES_PASS"
|
||||
envESDB = "MF_THINGS_ES_DB"
|
||||
envHTTPPort = "MF_THINGS_HTTP_PORT"
|
||||
envGRPCPort = "MF_THINGS_GRPC_PORT"
|
||||
envUsersURL = "MF_USERS_URL"
|
||||
envServerCert = "MF_THINGS_SERVER_CERT"
|
||||
envServerKey = "MF_THINGS_SERVER_KEY"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
logLevel string
|
||||
dbHost string
|
||||
dbPort string
|
||||
dbUser string
|
||||
dbPass string
|
||||
dbName string
|
||||
dbSSLMode 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
|
||||
@@ -100,15 +110,17 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
cache := connectToCache(cfg.cacheURL, cfg.cachePass, cfg.cacheDB, logger)
|
||||
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 := connectToUsers(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
svc := newService(conn, db, cache, logger)
|
||||
svc := newService(conn, db, cacheClient, esClient, logger)
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg, logger, errs)
|
||||
@@ -130,19 +142,29 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
Host: mainflux.Env(envDBHost, defDBHost),
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Name: mainflux.Env(envDBName, defDBName),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
|
||||
}
|
||||
|
||||
return config{
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
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),
|
||||
dbSSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
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),
|
||||
@@ -151,8 +173,7 @@ func loadConfig() config {
|
||||
}
|
||||
}
|
||||
|
||||
func connectToCache(cacheURL, cachePass string, cacheDB string, logger logger.Logger) *redis.Client {
|
||||
|
||||
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))
|
||||
@@ -166,8 +187,8 @@ func connectToCache(cacheURL, cachePass string, cacheDB string, logger logger.Lo
|
||||
})
|
||||
}
|
||||
|
||||
func connectToDB(cfg config, logger logger.Logger) *sql.DB {
|
||||
db, err := postgres.Connect(cfg.dbHost, cfg.dbPort, cfg.dbName, cfg.dbUser, cfg.dbPass, cfg.dbSSLMode)
|
||||
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)
|
||||
@@ -200,16 +221,16 @@ func connectToUsers(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
return conn
|
||||
}
|
||||
|
||||
func newService(conn *grpc.ClientConn, db *sql.DB, client *redis.Client, logger logger.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(client)
|
||||
thingCache := rediscache.NewThingCache(client)
|
||||
chanCache := rediscache.NewChannelCache(cacheClient)
|
||||
thingCache := rediscache.NewThingCache(cacheClient)
|
||||
idp := uuid.New()
|
||||
|
||||
svc := things.New(users, thingsRepo, channelsRepo, chanCache, thingCache, idp)
|
||||
svc = rediscache.NewEventStoreMiddleware(svc, client)
|
||||
svc = rediscache.NewEventStoreMiddleware(svc, esClient)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
@@ -235,10 +256,10 @@ func startHTTPServer(svc things.Service, cfg config, logger logger.Logger, errs
|
||||
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))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Things service started using http on port %s", cfg.httpPort))
|
||||
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc))
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Things service started using http on port %s", cfg.httpPort))
|
||||
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc))
|
||||
}
|
||||
|
||||
func startGRPCServer(svc things.Service, cfg config, logger logger.Logger, errs chan error) {
|
||||
@@ -246,6 +267,7 @@ func startGRPCServer(svc things.Service, cfg config, logger logger.Logger, errs
|
||||
listener, err := net.Listen("tcp", p)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to listen on port %s: %s", cfg.grpcPort, err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var server *grpc.Server
|
||||
|
||||
+67
-58
@@ -34,62 +34,63 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defLogLevel = "error"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "users"
|
||||
defDBSSLMode = "disable"
|
||||
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"
|
||||
envHTTPPort = "MF_USERS_HTTP_PORT"
|
||||
envGRPCPort = "MF_USERS_GRPC_PORT"
|
||||
envSecret = "MF_USERS_SECRET"
|
||||
envServerCert = "MF_USERS_SERVER_CERT"
|
||||
envServerKey = "MF_USERS_SERVER_KEY"
|
||||
defLogLevel = "error"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "users"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defHTTPPort = "8180"
|
||||
defGRPCPort = "8181"
|
||||
defSecret = "users"
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
envLogLevel = "MF_USERS_LOG_LEVEL"
|
||||
envDBHost = "MF_USERS_DB_HOST"
|
||||
envDBPort = "MF_USERS_DB_PORT"
|
||||
envDBUser = "MF_USERS_DB_USER"
|
||||
envDBPass = "MF_USERS_DB_PASS"
|
||||
envDBName = "MF_USERS_DB"
|
||||
envDBSSLMode = "MF_USERS_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_USERS_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_USERS_DB_SSL_KEY"
|
||||
envDBSSLRootCert = "MF_USERS_DB_SSL_ROOT_CERT"
|
||||
envHTTPPort = "MF_USERS_HTTP_PORT"
|
||||
envGRPCPort = "MF_USERS_GRPC_PORT"
|
||||
envSecret = "MF_USERS_SECRET"
|
||||
envServerCert = "MF_USERS_SERVER_CERT"
|
||||
envServerKey = "MF_USERS_SERVER_KEY"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
LogLevel string
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
DBSSLMode string
|
||||
HTTPPort string
|
||||
GRPCPort string
|
||||
Secret string
|
||||
ServerCert string
|
||||
ServerKey string
|
||||
logLevel string
|
||||
dbConfig postgres.Config
|
||||
httpPort string
|
||||
grpcPort string
|
||||
secret string
|
||||
serverCert string
|
||||
serverKey string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, err := logger.New(os.Stdout, cfg.LogLevel)
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
db := connectToDB(cfg, logger)
|
||||
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, cfg.ServerCert, cfg.ServerKey, logger, errs)
|
||||
go startGRPCServer(svc, cfg.GRPCPort, cfg.ServerCert, cfg.ServerKey, 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)
|
||||
@@ -102,24 +103,32 @@ func main() {
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
Host: mainflux.Env(envDBHost, defDBHost),
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Name: mainflux.Env(envDBName, defDBName),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
|
||||
}
|
||||
|
||||
return config{
|
||||
LogLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
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),
|
||||
DBSSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
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),
|
||||
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 logger.Logger) *sql.DB {
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass, cfg.DBSSLMode)
|
||||
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)
|
||||
@@ -155,10 +164,10 @@ func newService(db *sql.DB, secret string, logger logger.Logger) users.Service {
|
||||
func startHTTPServer(svc users.Service, port string, certFile string, keyFile string, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
if certFile != "" || keyFile != "" {
|
||||
logger.Info(fmt.Sprintf("Things service started using https, cert %s key %s, exposed port %s", certFile, keyFile, port))
|
||||
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("Things service started using http, exposed port %s", port))
|
||||
logger.Info(fmt.Sprintf("Users service started using http, exposed port %s", port))
|
||||
errs <- http.ListenAndServe(p, httpapi.MakeHandler(svc, logger))
|
||||
}
|
||||
}
|
||||
@@ -174,7 +183,7 @@ func startGRPCServer(svc users.Service, port string, certFile string, keyFile st
|
||||
if certFile != "" || keyFile != "" {
|
||||
creds, err := credentials.NewServerTLSFromFile(certFile, keyFile)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to load things certificates: %s", err))
|
||||
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))
|
||||
|
||||
+4
-4
@@ -47,9 +47,9 @@ var (
|
||||
type Broker interface {
|
||||
mainflux.MessagePublisher
|
||||
|
||||
// Subscribes to channel with specified id and adds subscription to
|
||||
// Subscribes to channel with specified id, subtopic and adds subscription to
|
||||
// service map of subscriptions under given ID.
|
||||
Subscribe(string, string, *Observer) error
|
||||
Subscribe(string, string, string, *Observer) error
|
||||
}
|
||||
|
||||
// Service specifies coap service API.
|
||||
@@ -135,8 +135,8 @@ func (svc *adapterService) Publish(msg mainflux.RawMessage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *adapterService) Subscribe(chanID, obsID string, o *Observer) error {
|
||||
if err := svc.pubsub.Subscribe(chanID, obsID, o); err != 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
|
||||
}
|
||||
|
||||
|
||||
+12
-4
@@ -32,7 +32,11 @@ func LoggingMiddleware(svc coap.Service, logger log.Logger) coap.Service {
|
||||
|
||||
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method publish to channel %s took %s to complete", msg.Channel, time.Since(begin))
|
||||
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
|
||||
@@ -43,9 +47,13 @@ func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) (err error) {
|
||||
return lm.svc.Publish(msg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Subscribe(chanID, obsID string, o *coap.Observer) (err error) {
|
||||
func (lm *loggingMiddleware) Subscribe(chanID, subtopic, obsID string, o *coap.Observer) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method subscribe to channel %s for client %s took %s to complete", chanID, obsID, time.Since(begin))
|
||||
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
|
||||
@@ -53,7 +61,7 @@ func (lm *loggingMiddleware) Subscribe(chanID, obsID string, o *coap.Observer) (
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Subscribe(chanID, obsID, o)
|
||||
return lm.svc.Subscribe(chanID, subtopic, obsID, o)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Unsubscribe(obsID string) {
|
||||
|
||||
+2
-2
@@ -43,13 +43,13 @@ func (mm *metricsMiddleware) Publish(msg mainflux.RawMessage) error {
|
||||
return mm.svc.Publish(msg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Subscribe(chanID, clientID string, o *coap.Observer) error {
|
||||
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, clientID, o)
|
||||
return mm.svc.Subscribe(chanID, subtopic, clientID, o)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Unsubscribe(clientID string) {
|
||||
|
||||
+52
-6
@@ -16,6 +16,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
@@ -33,11 +34,15 @@ import (
|
||||
const protocol = "coap"
|
||||
|
||||
var (
|
||||
errBadRequest = errors.New("bad request")
|
||||
errBadOption = errors.New("bad option")
|
||||
auth mainflux.ThingsServiceClient
|
||||
logger log.Logger
|
||||
pingPeriod time.Duration
|
||||
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
|
||||
@@ -68,7 +73,9 @@ func MakeCOAPHandler(svc coap.Service, tc mainflux.ThingsServiceClient, l log.Lo
|
||||
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
|
||||
@@ -109,6 +116,32 @@ func authorize(msg *gocoap.Message, res *gocoap.Message, cid string) (string, er
|
||||
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
|
||||
@@ -139,6 +172,12 @@ func receive(svc coap.Service) handler {
|
||||
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
|
||||
@@ -147,6 +186,7 @@ func receive(svc coap.Service) handler {
|
||||
|
||||
rawMsg := mainflux.RawMessage{
|
||||
Channel: chanID,
|
||||
Subtopic: subtopic,
|
||||
Publisher: publisher,
|
||||
Protocol: protocol,
|
||||
Payload: msg.Payload,
|
||||
@@ -177,6 +217,12 @@ func observe(svc coap.Service, responses chan<- string) handler {
|
||||
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
|
||||
@@ -198,7 +244,7 @@ func observe(svc coap.Service, responses chan<- string) handler {
|
||||
if value, ok := msg.Option(gocoap.Observe).(uint32); ok && value == 0 {
|
||||
res.AddOption(gocoap.Observe, 1)
|
||||
o := coap.NewObserver()
|
||||
if err := svc.Subscribe(chanID, obsID, o); err != nil {
|
||||
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
|
||||
|
||||
+12
-3
@@ -30,18 +30,27 @@ 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 := fmt.Sprintf("%s.%s", prefix, msg.Channel)
|
||||
subject := pubsub.fmtSubject(msg.Channel, msg.Subtopic)
|
||||
return pubsub.nc.Publish(subject, data)
|
||||
}
|
||||
|
||||
func (pubsub *natsPublisher) Subscribe(chanID, obsID string, observer *coap.Observer) error {
|
||||
sub, err := pubsub.nc.Subscribe(fmt.Sprintf("%s.%s", prefix, chanID), func(msg *broker.Msg) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2018
|
||||
Mainflux
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<form [formGroup]="addChannelForm" (ngSubmit)="onAddChannel()">
|
||||
<h1 *ngIf="!editMode" mat-dialog-title>Add channel</h1>
|
||||
<h1 *ngIf="editMode" mat-dialog-title>Edit channel</h1>
|
||||
<div mat-dialog-content fxLayout="column">
|
||||
<input type="hidden" formControlName="id">
|
||||
<mat-form-field>
|
||||
<input matInput tabindex="1" placeholder="Name" formControlName="name" />
|
||||
<mat-error *ngIf="addChannelForm.get('name').errors?.required">
|
||||
Name is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="Connected things" formControlName="connected" [compareWith]="compareFunction" multiple>
|
||||
<mat-option *ngFor="let thing of thingsStore.things" [value]="thing">{{thing.name}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addChannelForm.invalid">Ok</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,63 +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 { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
|
||||
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 { ChannelsService } from '../../../core/services/channels/channels.service';
|
||||
import { ThingsService } from '../../../core/services/things/things.service';
|
||||
import { ChannelDialogComponent } from './channel-dialog.component';
|
||||
import { ThingsStore } from '../../../core/store/things.store';
|
||||
import { UiStore } from '../../../core/store/ui.store';
|
||||
|
||||
describe('ChannelDialogComponent', () => {
|
||||
let component: ChannelDialogComponent;
|
||||
let fixture: ComponentFixture<ChannelDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ChannelDialogComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
ThingsStore,
|
||||
UiStore,
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ChannelDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { toJS } from 'mobx';
|
||||
|
||||
import { ThingsStore } from '../../../core/store/things.store';
|
||||
import { Channel, Thing } from '../../../core/store/models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-channel-dialog',
|
||||
templateUrl: './channel-dialog.component.html',
|
||||
styleUrls: ['./channel-dialog.component.scss']
|
||||
})
|
||||
export class ChannelDialogComponent implements OnInit {
|
||||
addChannelForm: FormGroup;
|
||||
@Output() submit: EventEmitter<Channel> = new EventEmitter<Channel>();
|
||||
editMode: boolean;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<ChannelDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: Channel,
|
||||
public thingsStore: ThingsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.addChannelForm = this.fb.group(
|
||||
{
|
||||
id: [''],
|
||||
name: [''],
|
||||
connected: [[]]
|
||||
}
|
||||
);
|
||||
|
||||
if (this.data) {
|
||||
this.editMode = true;
|
||||
this.addChannelForm.patchValue(toJS(this.data));
|
||||
} else {
|
||||
this.editMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
onAddChannel() {
|
||||
const channel = this.addChannelForm.value;
|
||||
this.submit.emit(channel);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
compareFunction(obj1: Thing, obj2: Thing) {
|
||||
return obj1.id === obj2.id;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2018
|
||||
Mainflux
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<div class="channelsContainer" fxLayout="row" fxLayoutWrap>
|
||||
<ng-container [ngSwitch]="channelsStore.channels?.length > 0">
|
||||
<ng-container *ngSwitchCase="true">
|
||||
<!-- List -->
|
||||
<div class="channel-list-container">
|
||||
<mat-table #table [dataSource]="channelsStore.channels.toJS()" matSort class="mat-cell">
|
||||
<!-- Columns -->
|
||||
<ng-container matColumnDef="id">
|
||||
<mat-header-cell fxFlex="32%" *matHeaderCellDef mat-sort-header>Id</mat-header-cell>
|
||||
<mat-cell fxFlex="32%" *matCellDef="let row" >{{row.id}}</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell fxFlex="30%" *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell fxFlex="30%" *matCellDef="let row"> {{row.name}}</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="connected">
|
||||
<mat-header-cell fxFlex="30%" *matHeaderCellDef mat-sort-header>Connected</mat-header-cell>
|
||||
<mat-cell fxFlex="30%" *matCellDef="let row">
|
||||
<li *ngFor="let item of row.connected"> {{item.id}} </li>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Actions -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<mat-header-cell fxFlex="8%" *matHeaderCellDef mat-sort-header></mat-header-cell>
|
||||
<mat-cell fxFlex="8%" *matCellDef="let row" >
|
||||
<button mat-icon-button color="accent" (click)="editChannel(row)">
|
||||
<mat-icon aria-label="Delete">edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="accent" (click)="deleteChannel(row)">
|
||||
<mat-icon aria-label="Delete">delete</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||
</mat-table>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchDefault>
|
||||
<h3>It looks like you don't have any channels in your account.</h3>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<button mat-mini-fab class="addButton" (click)="addChannel()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
.addButton {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
/* List container */
|
||||
.channel-list-container {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,75 +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 { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
|
||||
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 { ChannelsService } from '../../core/services/channels/channels.service';
|
||||
import { ThingsService } from '../../core/services/things/things.service';
|
||||
import { ChannelsComponent } from './channels.component';
|
||||
import { ThingsStore } from '../../core/store/things.store';
|
||||
import { UiStore } from '../../core/store/ui.store';
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
|
||||
describe('ChannelsComponent', () => {
|
||||
let component: ChannelsComponent;
|
||||
let fixture: ComponentFixture<ChannelsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ChannelsComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: ThingsStore,
|
||||
useClass: class {
|
||||
getThings = jasmine.createSpy('getThings');
|
||||
}
|
||||
},
|
||||
UiStore,
|
||||
{
|
||||
provide: ChannelsStore,
|
||||
useClass: class {
|
||||
getChannels = jasmine.createSpy('getChannels');
|
||||
}
|
||||
},
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ChannelsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { ChannelsStore } from '../../core/store/channels.store';
|
||||
import { ThingsStore } from '../../core/store/things.store';
|
||||
import { Channel } from '../../core/store/models';
|
||||
import { ConfirmationDialogComponent } from '../shared/confirmation-dialog/confirmation-dialog.component';
|
||||
import { ChannelDialogComponent } from './channel-dialog/channel-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-channels',
|
||||
templateUrl: './channels.component.html',
|
||||
styleUrls: ['./channels.component.scss'],
|
||||
})
|
||||
export class ChannelsComponent implements OnInit {
|
||||
channels: Observable<Channel[]>;
|
||||
displayedColumns = ['id', 'name', 'connected', 'actions'];
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
public thingsStore: ThingsStore,
|
||||
public channelsStore: ChannelsStore,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.channelsStore.getChannels();
|
||||
this.thingsStore.getThings();
|
||||
}
|
||||
|
||||
addChannel() {
|
||||
const dialogRef = this.dialog.open(ChannelDialogComponent);
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((channel: Channel) => {
|
||||
this.channelsStore.addChannel(channel);
|
||||
});
|
||||
}
|
||||
|
||||
editChannel(channel: Channel) {
|
||||
const dialogRef = this.dialog.open(ChannelDialogComponent, {
|
||||
data: channel
|
||||
});
|
||||
|
||||
dialogRef.componentInstance.submit.subscribe((editedChannel: Channel) => {
|
||||
this.channelsStore.editChannel(editedChannel);
|
||||
});
|
||||
}
|
||||
|
||||
deleteChannel(channel: Channel) {
|
||||
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
||||
data: {
|
||||
question: 'Are you sure you want to delete the channel?'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.channelsStore.deleteChannel(channel);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
<!--
|
||||
Copyright (c) 2018
|
||||
Mainflux
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<h1 mat-dialog-title>{{data.question}}</h1>
|
||||
<div mat-dialog-actions fxLayoutAlign="end end">
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button mat-raised-button color="primary" [mat-dialog-close]="true">Ok</button>
|
||||
</div>
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { MaterialModule } from '../../../core/material/material.module';
|
||||
import { ConfirmationDialogComponent } from './confirmation-dialog.component';
|
||||
|
||||
describe('ConfirmationDialogComponent', () => {
|
||||
let component: ConfirmationDialogComponent;
|
||||
let fixture: ComponentFixture<ConfirmationDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ConfirmationDialogComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConfirmationDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018
|
||||
* Mainflux
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirmation-dialog',
|
||||
templateUrl: './confirmation-dialog.component.html',
|
||||
styleUrls: ['./confirmation-dialog.component.scss']
|
||||
})
|
||||
export class ConfirmationDialogComponent implements OnInit {
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<form [formGroup]="addThingForm" (ngSubmit)="onAddThing()">
|
||||
<h1 mat-dialog-title>Add thing</h1>
|
||||
<div mat-dialog-content fxLayout="column">
|
||||
<input type="hidden" formControlName="id">
|
||||
<mat-form-field *ngIf="!editMode">
|
||||
<mat-select placeholder="Thing type" formControlName="type">
|
||||
<mat-option value="app">
|
||||
App
|
||||
</mat-option>
|
||||
<mat-option value="device">
|
||||
Device
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="addThingForm.get('type').errors?.required">
|
||||
Type is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput tabindex="1" placeholder="Name" formControlName="name" />
|
||||
<mat-error *ngIf="addThingForm.get('name').errors?.required">
|
||||
Name is required
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<textarea formControlName="metadata" matInput placeholder="Metadata" matTextareaAutosize matAutosizeMinRows="2"
|
||||
matAutosizeMaxRows="5"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<button mat-button mat-raised-button color="primary" type="submit" [disabled]="addThingForm.invalid">Ok</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,52 +0,0 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material';
|
||||
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 { ChannelsService } from '../../../core/services/channels/channels.service';
|
||||
import { ThingsService } from '../../../core/services/things/things.service';
|
||||
import { ThingDialogComponent } from './thing-dialog.component';
|
||||
|
||||
describe('ThingDialogComponent', () => {
|
||||
let component: ThingDialogComponent;
|
||||
let fixture: ComponentFixture<ThingDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ThingDialogComponent ],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
MatDialogModule,
|
||||
HttpClientModule,
|
||||
RouterTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
AuthenticationService,
|
||||
TokenStorage,
|
||||
ThingsService,
|
||||
ChannelsService,
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: [] },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ThingDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user