mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 436db8877d | |||
| 4da66aecbf | |||
| 3c794d4a03 | |||
| 8dd59f8557 | |||
| bc41c32cf5 | |||
| 42e4e054c2 | |||
| 9ed5f8334f | |||
| 8ea26c5ab7 | |||
| 043d1e0aac | |||
| f10e49e6b5 | |||
| f18f2c1c98 | |||
| dfa6d8ba0d | |||
| e272d9f7ad | |||
| 26c944b5c3 | |||
| 3694a82de6 | |||
| 930f8beb29 | |||
| 9334568ba2 | |||
| b91024465c | |||
| be1361311d | |||
| c1088b9315 | |||
| 2453cd75ed | |||
| 7dc6b0d879 | |||
| 381ebb1e51 | |||
| 09d09c6ef5 | |||
| a7eee53dfb | |||
| e6b7fd818b | |||
| ecff066421 | |||
| 8d33285489 | |||
| ee5c76395a | |||
| 4a9c7403d8 | |||
| 381a15a695 | |||
| a7b81dc37a | |||
| a1643b1e7e | |||
| 340e685d70 | |||
| 97f8d65885 | |||
| 7f3e2c1b21 | |||
| 8b004b3daf | |||
| a7c3cfcf1c | |||
| f1ee9d0665 | |||
| ea3a891c91 | |||
| c26c7f34cd | |||
| a5fb55c328 | |||
| 8906943d1d | |||
| a57fb03c01 | |||
| 877005538c | |||
| b4c80132e6 | |||
| d7670e7adb | |||
| df6f5adff8 | |||
| 5089fccf36 | |||
| b13a3135dd | |||
| 4d9694c027 | |||
| b8be18129c | |||
| f9432c6525 | |||
| f482df9033 | |||
| 16ac44f377 | |||
| 2b4cf8a990 | |||
| 0c016cd7ce | |||
| f872546925 | |||
| 9d5202c46b | |||
| bf189fbd23 | |||
| c3019fffb6 | |||
| 36d00d8b9e | |||
| 9b4eb7056f | |||
| cb3b70d567 | |||
| c7a8e75353 | |||
| 9d430fa02c | |||
| 7a18d3fa56 | |||
| ccbfe20607 | |||
| eb9092494e | |||
| 88fa724fc3 | |||
| 7910d64ad4 | |||
| 01aa4190c5 | |||
| f982b8ccc6 | |||
| 7d839b7b61 | |||
| 6bd74575ff | |||
| 4c970a8079 | |||
| b8bd011f9d | |||
| fbbf2c07a5 | |||
| 5b5229975b | |||
| d8dca28072 | |||
| 537d73d0c6 | |||
| 880e193b0a | |||
| e438be4250 | |||
| 57f7ee2595 | |||
| 6c39c85cc8 | |||
| 338c55e554 | |||
| 382bc89161 | |||
| 8325c1caf6 | |||
| 1d78233fe6 | |||
| fff492bd50 | |||
| 51ec256664 | |||
| c20b43ed41 | |||
| 46aadcfd9d | |||
| d2153a8846 | |||
| 70955d1937 | |||
| c1df2cb21c | |||
| b8818c4dd2 | |||
| a2d70c8907 | |||
| 6ffa916ed2 | |||
| 19503742a6 | |||
| c91fe0d453 | |||
| 3e4a9eb16a | |||
| b3f91f5c9e | |||
| ba97e865a4 | |||
| cc5e0288df | |||
| 6e4e5b351a | |||
| d587921eda | |||
| 7df2ababc4 | |||
| 63dc1f69b6 | |||
| 1c6f124062 | |||
| cc90f568b6 | |||
| 5e145858f1 | |||
| 9e2bb4933a | |||
| 3f77b87a91 | |||
| dcba41e8c4 | |||
| f6c813ea8c | |||
| 42b3682352 | |||
| 3b5d51276f | |||
| a30a3b9063 | |||
| e22d1fbbb4 | |||
| 51cd0524a1 | |||
| 1c2c86b030 | |||
| a1078e6999 | |||
| b8138fac32 | |||
| 2b7637fd75 | |||
| f785116a6f | |||
| 3f6a0cd14b | |||
| 269fe89a42 | |||
| bdf1326933 | |||
| a96db05cc0 | |||
| 7b62f1ce8a | |||
| 575d1d6bbf | |||
| 0d361f3df1 | |||
| 5714a6dfcb | |||
| decfae9c9d | |||
| c4fa27fd7e | |||
| 8475e87fe7 | |||
| 0d3adfdc5c | |||
| 160fda4d37 | |||
| 4ba1717582 | |||
| b3991b8497 | |||
| 5834d364ad | |||
| f42f2095ba | |||
| 60a90d84d5 | |||
| 887bc2d9c0 |
@@ -2,8 +2,5 @@
|
||||
.github
|
||||
build
|
||||
docker
|
||||
docs
|
||||
k8s
|
||||
load-test
|
||||
metrics
|
||||
scripts
|
||||
|
||||
@@ -12,9 +12,6 @@ MF_NATS_URL=nats://nats:4222
|
||||
## Redis
|
||||
MF_REDIS_TCP_PORT=6379
|
||||
|
||||
## UI
|
||||
MF_UI_PORT=3000
|
||||
|
||||
## Grafana
|
||||
MF_GRAFANA_PORT=3000
|
||||
|
||||
@@ -31,6 +28,8 @@ MF_JAEGER_URL=jaeger:6831
|
||||
MF_AUTHN_LOG_LEVEL=debug
|
||||
MF_AUTHN_HTTP_PORT=8189
|
||||
MF_AUTHN_GRPC_PORT=8181
|
||||
MF_AUTHN_GRPC_URL=authn:8181
|
||||
MF_AUTHN_GRPC_TIMEOUT=1s
|
||||
MF_AUTHN_DB_PORT=5432
|
||||
MF_AUTHN_DB_USER=mainflux
|
||||
MF_AUTHN_DB_PASS=mainflux
|
||||
@@ -44,6 +43,8 @@ MF_USERS_DB_PORT=5432
|
||||
MF_USERS_DB_USER=mainflux
|
||||
MF_USERS_DB_PASS=mainflux
|
||||
MF_USERS_DB=users
|
||||
MF_USERS_ADMIN_EMAIL=admin@example.com
|
||||
MF_USERS_ADMIN_PASSWORD=12345678
|
||||
|
||||
### Email utility
|
||||
MF_EMAIL_DRIVER=smtp
|
||||
@@ -63,26 +64,31 @@ MF_THINGS_LOG_LEVEL=debug
|
||||
MF_THINGS_HTTP_PORT=8182
|
||||
MF_THINGS_AUTH_HTTP_PORT=8989
|
||||
MF_THINGS_AUTH_GRPC_PORT=8183
|
||||
MF_THINGS_AUTH_GRPC_URL=things:8183
|
||||
MF_THINGS_AUTH_GRPC_TIMEOUT=1s
|
||||
MF_THINGS_DB_PORT=5432
|
||||
MF_THINGS_DB_USER=mainflux
|
||||
MF_THINGS_DB_PASS=mainflux
|
||||
MF_THINGS_DB=things
|
||||
MF_THINGS_SECRET=secret
|
||||
MF_THINGS_ES_URL=localhost:6379
|
||||
MF_THINGS_ES_PASS=
|
||||
MF_THINGS_ES_DB=0
|
||||
|
||||
### WS
|
||||
MF_WS_ADAPTER_LOG_LEVEL=debug
|
||||
MF_WS_ADAPTER_PORT=8186
|
||||
|
||||
### HTTP
|
||||
MF_HTTP_ADAPTER_PORT=8185
|
||||
|
||||
### MQTT
|
||||
MF_MQTT_ADAPTER_LOG_LEVEL=debug
|
||||
MF_MQTT_ADAPTER_PORT=1883
|
||||
MF_MQTT_ADAPTER_MQTT_PORT=1883
|
||||
MF_MQTT_BROKER_PORT=1883
|
||||
MF_MQTT_ADAPTER_WS_PORT=8080
|
||||
MF_MQTT_BROKER_WS_PORT=8080
|
||||
MF_MQTT_ADAPTER_ES_DB=0
|
||||
MF_MQTT_ADAPTER_ES_PASS=
|
||||
|
||||
### VERMEMQ
|
||||
MF_DOCKER_VERNEMQ_ALLOW_ANONYMOUS=on
|
||||
MF_DOCKER_VERNEMQ_LOG__CONSOLE__LEVEL=error
|
||||
|
||||
### CoAP
|
||||
MF_COAP_ADAPTER_LOG_LEVEL=debug
|
||||
@@ -98,6 +104,58 @@ MF_BOOTSTRAP_DB_PASS=mainflux
|
||||
MF_BOOTSTRAP_DB=bootstrap
|
||||
MF_BOOTSTRAP_DB_SSL_MODE=disable
|
||||
|
||||
### Provision
|
||||
MF_PROVISION_CONFIG_FILE=/configs/config.toml
|
||||
MF_PROVISION_LOG_LEVEL=debug
|
||||
MF_PROVISION_HTTP_PORT=8190
|
||||
MF_PROVISION_ENV_CLIENTS_TLS=false
|
||||
MF_PROVISION_SERVER_CERT=
|
||||
MF_PROVISION_SERVER_KEY=
|
||||
MF_PROVISION_MQTT_URL=tcp://localhost
|
||||
MF_PROVISION_USERS_LOCATION=http://localhost:8180
|
||||
MF_PROVISION_THINGS_LOCATION=http://things:8182
|
||||
MF_PROVISION_USER=
|
||||
MF_PROVISION_PASS=
|
||||
MF_PROVISION_API_KEY=
|
||||
MF_PROVISION_CERTS_SVC_URL=http://certs:8204
|
||||
MF_PROVISION_X509_PROVISIONING=true
|
||||
MF_PROVISION_BS_SVC_URL=http://bootstrap:8202/things
|
||||
MF_PROVISION_BS_SVC_WHITELIST_URL=http://bootstrap:8202/things/state
|
||||
MF_PROVISION_BS_CONFIG_PROVISIONING=true
|
||||
MF_PROVISION_BS_AUTO_WHITELIST=true
|
||||
MF_PROVISION_BS_CONTENT=
|
||||
MF_PROVISION_CERTS_RSA_BITS=4096
|
||||
MF_PROVISION_CERTS_HOURS_VALID=2400h
|
||||
|
||||
# Certs
|
||||
MF_CERTS_LOG_LEVEL=debug
|
||||
MF_CERTS_HTTP_PORT=8204
|
||||
MF_CERTS_DB_HOST=certs-db
|
||||
MF_CERTS_DB_PORT=5432
|
||||
MF_CERTS_DB_USER=mainflux
|
||||
MF_CERTS_DB_PASS=mainflux
|
||||
MF_CERTS_DB=certs
|
||||
MF_CERTS_DB_SSL_MODE=
|
||||
MF_CERTS_DB_SSL_CERT=
|
||||
MF_CERTS_DB_SSL_KEY=
|
||||
MF_CERTS_DB_SSL_ROOT_CERT=
|
||||
MF_CERTS_ENCRYPT_KEY=
|
||||
MF_CERTS_CLIENT_TLS=
|
||||
MF_CERTS_CA_CERTS=
|
||||
MF_CERTS_SERVER_CERT=
|
||||
MF_CERTS_SERVER_KEY=
|
||||
MF_SDK_BASE_URL=http://172.17.0.1
|
||||
MF_SDK_THINGS_PREFIX=
|
||||
MF_CERTS_SIGN_CA_PATH=/etc/ssl/certs/ca.crt
|
||||
MF_CERTS_SIGN_CA_KEY_PATH=/etc/ssl/certs/ca.key
|
||||
MF_CERTS_SIGN_HOURS_VALID=2048h
|
||||
MF_CERTS_SIGN_RSA_BITS=2048
|
||||
MF_CERTS_VAULT_HOST=
|
||||
MF_CERTS_VAULT_PKI_PATH=pki_int
|
||||
MF_CERTS_VAULT_ROLE=agent
|
||||
MF_CERTS_VAULT_TOKEN=s.nArgw6xn3uIOfA7nfKk8LFaW
|
||||
|
||||
|
||||
### LoRa
|
||||
MF_LORA_ADAPTER_LOG_LEVEL=debug
|
||||
MF_LORA_ADAPTER_MESSAGES_URL=tcp://lora.mqtt.mainflux.io:1883
|
||||
@@ -109,9 +167,6 @@ MF_LORA_ADAPTER_ROUTE_MAP_DB=0
|
||||
### OPC-UA
|
||||
MF_OPCUA_ADAPTER_HTTP_PORT=8188
|
||||
MF_OPCUA_ADAPTER_LOG_LEVEL=debug
|
||||
MF_OPCUA_ADAPTER_SERVER_URI=opc.tcp://opcua.rocks:4840
|
||||
MF_OPCUA_ADAPTER_NODE_NAMESPACE=0
|
||||
MF_OPCUA_ADAPTER_NODE_IDENTIFIER=2256
|
||||
MF_OPCUA_ADAPTER_POLICY=
|
||||
MF_OPCUA_ADAPTER_MODE=
|
||||
MF_OPCUA_ADAPTER_CERT_FILE=
|
||||
@@ -127,6 +182,7 @@ MF_CASSANDRA_WRITER_PORT=8902
|
||||
MF_CASSANDRA_WRITER_DB_PORT=9042
|
||||
MF_CASSANDRA_WRITER_DB_CLUSTER=mainflux-cassandra
|
||||
MF_CASSANDRA_WRITER_DB_KEYSPACE=mainflux
|
||||
MF_CASSANDRA_WRITER_CONTENT_TYPE=application/senml+json
|
||||
|
||||
### Cassandra Reader
|
||||
MF_CASSANDRA_READER_LOG_LEVEL=debug
|
||||
@@ -134,6 +190,8 @@ MF_CASSANDRA_READER_PORT=8903
|
||||
MF_CASSANDRA_READER_DB_PORT=9042
|
||||
MF_CASSANDRA_READER_DB_CLUSTER=mainflux-cassandra
|
||||
MF_CASSANDRA_READER_DB_KEYSPACE=mainflux
|
||||
MF_CASSANDRA_READER_SERVER_CERT=
|
||||
MF_CASSANDRA_READER_SERVER_KEY=
|
||||
|
||||
### InfluxDB Writer
|
||||
MF_INFLUX_WRITER_LOG_LEVEL=debug
|
||||
@@ -141,30 +199,36 @@ MF_INFLUX_WRITER_PORT=8900
|
||||
MF_INFLUX_WRITER_BATCH_SIZE=5000
|
||||
MF_INFLUX_WRITER_BATCH_TIMEOUT=5
|
||||
MF_INFLUX_WRITER_DB_PORT=8086
|
||||
MF_INFLUX_WRITER_DB_NAME=mainflux
|
||||
MF_INFLUX_WRITER_DB_USER=mainflux
|
||||
MF_INFLUX_WRITER_DB_PASS=mainflux
|
||||
MF_INFLUX_WRITER_DB=mainflux
|
||||
MF_INFLUX_WRITER_GRAFANA_PORT=3001
|
||||
MF_INFLUX_WRITER_CONTENT_TYPE=application/senml+json
|
||||
|
||||
### InfluxDB Reader
|
||||
MF_INFLUX_READER_LOG_LEVEL=debug
|
||||
MF_INFLUX_READER_PORT=8905
|
||||
MF_INFLUX_READER_DB_NAME=mainflux
|
||||
MF_INFLUX_READER_DB_PORT=8086
|
||||
MF_INFLUX_READER_DB_USER=mainflux
|
||||
MF_INFLUX_READER_DB_PASS=mainflux
|
||||
MF_INFLUX_READER_DB=mainflux
|
||||
MF_INFLUX_READER_SERVER_CERT=
|
||||
MF_INFLUX_READER_SERVER_KEY=
|
||||
|
||||
### MongoDB Writer
|
||||
MF_MONGO_WRITER_LOG_LEVEL=debug
|
||||
MF_MONGO_WRITER_PORT=8901
|
||||
MF_MONGO_WRITER_DB_NAME=mainflux
|
||||
MF_MONGO_WRITER_DB=mainflux
|
||||
MF_MONGO_WRITER_DB_PORT=27017
|
||||
MF_MONGO_WRITER_CONTENT_TYPE=application/senml+json
|
||||
|
||||
### MongoDB Reader
|
||||
MF_MONGO_READER_LOG_LEVEL=debug
|
||||
MF_MONGO_READER_PORT=8904
|
||||
MF_MONGO_READER_DB_NAME=mainflux
|
||||
MF_MONGO_READER_DB=mainflux
|
||||
MF_MONGO_READER_DB_PORT=27017
|
||||
MF_MONGO_READER_SERVER_CERT=
|
||||
MF_MONGO_READER_SERVER_KEY=
|
||||
|
||||
### Postgres Writer
|
||||
MF_POSTGRES_WRITER_LOG_LEVEL=debug
|
||||
@@ -172,11 +236,12 @@ MF_POSTGRES_WRITER_PORT=9104
|
||||
MF_POSTGRES_WRITER_DB_PORT=5432
|
||||
MF_POSTGRES_WRITER_DB_USER=mainflux
|
||||
MF_POSTGRES_WRITER_DB_PASS=mainflux
|
||||
MF_POSTGRES_WRITER_DB_NAME=messages
|
||||
MF_POSTGRES_WRITER_DB=mainflux
|
||||
MF_POSTGRES_WRITER_DB_SSL_MODE=disable
|
||||
MF_POSTGRES_WRITER_DB_SSL_CERT=""
|
||||
MF_POSTGRES_WRITER_DB_SSL_KEY=""
|
||||
MF_POSTGRES_WRITER_DB_SSL_ROOT_CERT=""
|
||||
MF_POSTGRES_WRITER_CONTENT_TYPE=application/senml+json
|
||||
|
||||
### Postgres Reader
|
||||
MF_POSTGRES_READER_LOG_LEVEL=debug
|
||||
@@ -186,8 +251,25 @@ MF_POSTGRES_READER_CA_CERTS=""
|
||||
MF_POSTGRES_READER_DB_PORT=5432
|
||||
MF_POSTGRES_READER_DB_USER=mainflux
|
||||
MF_POSTGRES_READER_DB_PASS=mainflux
|
||||
MF_POSTGRES_READER_DB_NAME=messages
|
||||
MF_POSTGRES_READER_DB=mainflux
|
||||
MF_POSTGRES_READER_DB_SSL_MODE=disable
|
||||
MF_POSTGRES_READER_DB_SSL_CERT=""
|
||||
MF_POSTGRES_READER_DB_SSL_KEY=""
|
||||
MF_POSTGRES_READER_DB_SSL_ROOT_CERT=""
|
||||
|
||||
# Twins
|
||||
MF_TWINS_LOG_LEVEL=debug
|
||||
MF_TWINS_HTTP_PORT=9021
|
||||
MF_TWINS_SERVER_CERT=""
|
||||
MF_TWINS_SERVER_KEY=""
|
||||
MF_TWINS_DB=mainflux-twins
|
||||
MF_TWINS_DB_HOST=twins-db
|
||||
MF_TWINS_DB_PORT=27018
|
||||
MF_TWINS_SINGLE_USER_EMAIL=""
|
||||
MF_TWINS_SINGLE_USER_TOKEN=""
|
||||
MF_TWINS_CLIENT_TLS=""
|
||||
MF_TWINS_CA_CERTS=""
|
||||
MF_TWINS_CHANNEL_ID=
|
||||
MF_TWINS_CACHE_URL=es-redis:6379
|
||||
MF_TWINS_CACHE_PASS=
|
||||
MF_TWINS_CACHE_DB=0
|
||||
|
||||
+105
-2
@@ -11,6 +11,110 @@ Otherwise, whole log in a similar format can be observed via:
|
||||
git log --pretty=oneline --abbrev-commit
|
||||
```
|
||||
|
||||
## 0.11.0 - 29. MAY 2020.
|
||||
### Features and Bugfixes
|
||||
- Add VerneMQ docker image build from source (#1178)
|
||||
- MF-994 - Add tracing middleware for twins and states repos (#1181)
|
||||
- MF-995 - Add Twins tests for endpoint list twins and list states (#1174)
|
||||
- NOISSUE - Update dependencies (#1176)
|
||||
- MF-1163 - Fix influxdb-reader to use nanoseconds precision (#1171)
|
||||
- Rename environment variable MF_MQTT_ADAPTER_PORT to MF_MQTT_ADAPTER_MQTT_PORT in docker environment (#1170)
|
||||
- Remove thing related code from twins service (#1169)
|
||||
- MF-997 - Add twins service swagger file (#1167)
|
||||
- MF-1079 - Add MQTT forwarder (#1164)
|
||||
- MF-1159 - add gateway metadata update in provision method (#1160)
|
||||
- MF-1055 - rollback/release transaction on error (#1166)
|
||||
- NOISSUE - Use log level error for VermeMQ docker (#1162)
|
||||
- NOISSUE - Fix default nats pubsub subject (#1153)
|
||||
- MF-1125 - Document Provision service (#1143)
|
||||
- NOISSUE - Fix bootstrap SDK args naming (#1151)
|
||||
- Use VerneMQ default log level (#1150)
|
||||
- NOISSUE - Update provision service (#1133)
|
||||
- NOISSUE - Refactor messaging (#1141)
|
||||
- Add JSON tags to SDK entities (#1146)
|
||||
- NOISSUE - Update CLI README.md (#1139)
|
||||
- NOISSUE - Update mProxy version (#1137)
|
||||
- fix nginx, channel connect (#1136)
|
||||
- Remove concurrency flag for golangci-lint (#1134)
|
||||
- MF-1088 - Remove message payload content type (#1121)
|
||||
- MF-1129 - Use snake_case for Lora and OPC-UA metadata fields (#1130)
|
||||
- MF-1128 - Add golangci-linter to a CI script (#1131)
|
||||
- MF-1123 - Move Provision service to monorepo (#1132)
|
||||
- MF-845 - Add FOSSA badge for licensing (#1127)
|
||||
- MF-1087 - Remove WebSocket adapter (#1120)
|
||||
- NOISSUE - Use HTTP Status in SDK error messages (#1119)
|
||||
- NOISSUE - Fix bootstrap token naming and interfaces named args (#1117)
|
||||
- MF-1115 - Improve the SDK error encoding (#1118)
|
||||
- MF-862 - Add boostrap CRUD to SDK and CLI (#1114)
|
||||
- NOISSUE - Update coding style in Things service (#1116)
|
||||
- NOISSUE - Remove defers from TestMain (#1111)
|
||||
- NOISSUE - Create func to encode SDK errors (#1110)
|
||||
- MF-1078 - Add timestamp to published messages and use it in Transformer (#1106)
|
||||
- Fix prometheus namespace in postgres reader & writer (#1109)
|
||||
- NOISSUE - Implement errors package in senml transformer, readers and writers (#1108)
|
||||
- NOISSUE - Implement errors package in Authentication service (#1105)
|
||||
- MF-1103 - API key should ignore empty expiration time (#1104)
|
||||
- MF-1096 - Fix AuthN and Things Auth ENVARS (#1066)
|
||||
- fix Contains function for nil arguments (#1102)
|
||||
- MF-1099 - Add email subdomain validator (#1101)
|
||||
- MF-1091 - Use channels. as broker prefix (#1098)
|
||||
- MF-1090 - Use named Interfaces args (#1097)
|
||||
- NOISSUE - Create broker package for NATS (#1080)
|
||||
- NOISSUE - Implement errors package in bootstrap service (#1093)
|
||||
- NOISSUE - Fix writers loadSubjectsConfig if file is missing (#1094)
|
||||
- NOISSUE - Adding subtopics filtering in writer services (#1072)
|
||||
- NOISSUE - Improve errors package (#1086)
|
||||
- NOISSUE - Enable MQTT over WS in docker composition (#1085)
|
||||
- NOISSUE - Rm unused opc-ua envars (#1083)
|
||||
- MF-798 - Add utf8 support for email validation (#1082)
|
||||
- Remove unused Tokenizer interface (#1084)
|
||||
- Update mqtt adapter imports (#1081)
|
||||
- NOISSUE - Update state based on SenML time value (#1075)
|
||||
- NOISSUE - Fix StatusBadDecodingError for opc-ua browse (#1074)
|
||||
- Save senml array msg to multiple states (#1073)
|
||||
- NOISSUE - Fix opc-ua message type handling (#1071)
|
||||
- NOISSUE - Add Publisher field to MQTT adapter (#1067)
|
||||
- NOISSUE - Fix users CLI (#1062)
|
||||
- NOISSUE - Fix SDK Messages response (#1064)
|
||||
- Merged MQTT docker compose in core composition file (#1060)
|
||||
- MF-1016 - Add UserUpdate and UpdatePassword to sdk and CLI (#1057)
|
||||
- Update mProxy (#1058)
|
||||
- MF-1053 - Add disconnect event to MQTT adapter (#1056)
|
||||
- Fix data type for data_value in databases (#1054)
|
||||
- NOISSUE - Fix opc-ua subscriptions store (#1052)
|
||||
- NOISSUE - Fix connect CLI command and remove ConnectThing func from SDK (#1051)
|
||||
- NOISSUE - Update Vernemq image repository (#1050)
|
||||
- Removed VerneMQ auth plugin, Aedes impl. Added mproxy support in docker (#1049)
|
||||
- NOISSUE - Add default subscription nodeID and Interval ENVAR (#1046)
|
||||
- MF-415 - Merge mProxy support (#1045)
|
||||
- NOISSUE - Remove twins-service mqtt dependency and publish notifs to nats (#1042)
|
||||
- Add arbitrary SenML value type saving to twin state (#1039)
|
||||
- Fixed Aedes dependencies (#1036)
|
||||
- MF-998 - Add Twins service to Makefile and docker-compose.yml (#1035)
|
||||
- MF-1032 - Fix redis docker volume of opcua-adapter (#1033)
|
||||
- NOISSUE - add nats conf (#1031)
|
||||
- MF-442 - Add SSL encryption to the MongoDB, InfluxDB and Cassanda readers (#1024)
|
||||
- NOISSUE - Add opc-ua type handling and unsubscription (#1029)
|
||||
- NOISSUE - Add aggregate attribute-based search for twin retrieval (#1027)
|
||||
- NOISSUE - Fix metadata in add Things endpoint (#1028)
|
||||
- NOISSUE - Fix minimal password length (#1023)
|
||||
- MF-1020 - Change default password for CLI provision test (#1021)
|
||||
- NOISSUE - Add subtopic to opcua messages (#1022)
|
||||
- NOISSUE - Add details to browsed OPC-UA nodes (#1019)
|
||||
- NOISSUE Fix obsolete attribute persistance (#1018)
|
||||
- Fix twins update revision counter (#1011)
|
||||
- Fixed docs instructions in README (#1010)
|
||||
- Fix copyright year (#1009)
|
||||
- Fix issuing recovery key (#1007)
|
||||
- Removed gatling load-test (#1005)
|
||||
- Removed old k8s manifests (#1004)
|
||||
- NOISSUE - Remove UI from docker-compose (#1001)
|
||||
- NOISSUE - Store successfull OPC-UA subscriptions (#999)
|
||||
- MF-730 - Add digital twin service for things (#855)
|
||||
- Fix Redis event naming (#996)
|
||||
- NOISSUE - Add a Browse endpoint in opcua-adapter (#988)
|
||||
- NOISSUE - Add Redis ES Username/Pass for VerneMQ (#991)
|
||||
- MF-982 - Add error when connecting empty channels or things (#985)
|
||||
|
||||
## 0.10.0 - 17. DEC 2019.
|
||||
### Features
|
||||
@@ -83,9 +187,8 @@ git log --pretty=oneline --abbrev-commit
|
||||
- NOISSUE - Add VerneMQ support (#809)
|
||||
- NOISSUE - Add content type as part of MQTT subscription topic (#810)
|
||||
|
||||
|
||||
### Bugfixes
|
||||
- Fix MQTT protobuf filename(#981)
|
||||
- Fix MQTT protobuf filename(#981)
|
||||
- MF-950 - Runtime error in normalizer - CBOR SenML (#974)
|
||||
- NOISSUE - Fix opcua-adapter events warnings (#965)
|
||||
- NOISSUE - Fix opcua-adapter events decode (#951)
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015-2019 Mainflux
|
||||
Copyright 2015-2020 Mainflux
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Copyright (c) Mainflux
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
MF_DOCKER_IMAGE_NAME_PREFIX ?= mainflux
|
||||
BUILD_DIR = build
|
||||
SERVICES = users things http ws coap lora influxdb-writer influxdb-reader mongodb-writer \
|
||||
SERVICES = users things http coap lora influxdb-writer influxdb-reader mongodb-writer \
|
||||
mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader cli \
|
||||
bootstrap opcua authn
|
||||
bootstrap opcua authn twins mqtt provision certs
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
@@ -22,7 +23,7 @@ define make_docker
|
||||
--build-arg SVC=$(svc) \
|
||||
--build-arg GOARCH=$(GOARCH) \
|
||||
--build-arg GOARM=$(GOARM) \
|
||||
--tag=mainflux/$(svc) \
|
||||
--tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile .
|
||||
endef
|
||||
|
||||
@@ -32,36 +33,24 @@ define make_docker_dev
|
||||
docker build \
|
||||
--no-cache \
|
||||
--build-arg SVC=$(svc) \
|
||||
--tag=mainflux/$(svc) \
|
||||
--tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile.dev ./build
|
||||
endef
|
||||
|
||||
all: $(SERVICES) mqtt
|
||||
all: $(SERVICES)
|
||||
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release mqtt
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
rm -rf mqtt/aedes/node_modules
|
||||
|
||||
cleandocker:
|
||||
# Stop all containers (if running)
|
||||
docker-compose -f docker/docker-compose.yml stop
|
||||
# Remove mainflux containers
|
||||
docker ps -f name=mainflux -aq | xargs -r docker rm
|
||||
|
||||
# Remove exited containers
|
||||
docker ps -f name=mainflux -f status=dead -f status=exited -aq | xargs -r docker rm -v
|
||||
|
||||
# Remove unused images
|
||||
docker images "mainflux\/*" -f dangling=true -q | xargs -r docker rmi
|
||||
|
||||
# Remove old mainflux images
|
||||
docker images -q mainflux\/* | xargs -r docker rmi
|
||||
# Stops containers and removes containers, networks, volumes, and images created by up
|
||||
docker-compose -f docker/docker-compose.yml down --rmi all -v --remove-orphans
|
||||
|
||||
ifdef pv
|
||||
# Remove unused volumes
|
||||
docker volume ls -f name=mainflux -f dangling=true -q | xargs -r docker volume rm
|
||||
docker volume ls -f name=$(MF_DOCKER_IMAGE_NAME_PREFIX) -f dangling=true -q | xargs -r docker volume rm
|
||||
endif
|
||||
|
||||
install:
|
||||
@@ -72,6 +61,7 @@ test:
|
||||
|
||||
proto:
|
||||
protoc --gofast_out=plugins=grpc:. *.proto
|
||||
protoc --gofast_out=plugins=grpc:. pkg/messaging/*.proto
|
||||
|
||||
$(SERVICES):
|
||||
$(call compile_service,$(@))
|
||||
@@ -82,29 +72,13 @@ $(DOCKERS):
|
||||
$(DOCKERS_DEV):
|
||||
$(call make_docker_dev,$(@))
|
||||
|
||||
docker_mqtt:
|
||||
# MQTT Docker build must be done from root dir because it copies .proto files
|
||||
ifeq ($(GOARCH), arm)
|
||||
docker build --tag=mainflux/mqtt -f mqtt/aedes/Dockerfile.arm .
|
||||
else
|
||||
docker build --tag=mainflux/mqtt -f mqtt/aedes/Dockerfile .
|
||||
endif
|
||||
|
||||
docker_mqtt_verne:
|
||||
docker build --tag=mainflux/mqtt-verne -f mqtt/verne/Dockerfile .
|
||||
|
||||
dockers: $(DOCKERS) docker_mqtt
|
||||
|
||||
dockers: $(DOCKERS)
|
||||
dockers_dev: $(DOCKERS_DEV)
|
||||
|
||||
mqtt:
|
||||
cd mqtt/aedes && npm install
|
||||
|
||||
define docker_push
|
||||
for svc in $(SERVICES); do \
|
||||
docker push mainflux/$$svc:$(1); \
|
||||
docker push $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(1); \
|
||||
done
|
||||
docker push mainflux/mqtt:$(1)
|
||||
endef
|
||||
|
||||
changelog:
|
||||
@@ -118,25 +92,22 @@ release:
|
||||
git checkout $(version)
|
||||
$(MAKE) dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag mainflux/$$svc mainflux/$$svc:$(version); \
|
||||
docker tag $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(version); \
|
||||
done
|
||||
docker tag mainflux/ui mainflux/ui:$(version)
|
||||
docker tag mainflux/mqtt mainflux/mqtt:$(version)
|
||||
$(call docker_push,$(version))
|
||||
|
||||
rundev:
|
||||
cd scripts && ./run.sh
|
||||
|
||||
run:
|
||||
docker-compose -f docker/docker-compose.yml -f docker/aedes.yml up
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
|
||||
runlora:
|
||||
docker-compose \
|
||||
-f docker/docker-compose.yml \
|
||||
-f docker/aedes.yml up \
|
||||
-f docker/addons/influxdb-writer/docker-compose.yml \
|
||||
-f docker/addons/lora-adapter/docker-compose.yml up \
|
||||
|
||||
# Run all Mainflux core services except distributed tracing system - Jaeger. Recommended on gateways:
|
||||
rungw:
|
||||
MF_JAEGER_URL= docker-compose -f docker/docker-compose.yml -f docker/aedes.yml up --scale jaeger=0
|
||||
MF_JAEGER_URL= docker-compose -f docker/docker-compose.yml up --scale jaeger=0
|
||||
|
||||
@@ -53,7 +53,7 @@ Developing Mainflux will also require:
|
||||
Once the prerequisites are installed, execute the following commands from the project's root:
|
||||
|
||||
```bash
|
||||
docker-compose -f docker/docker-compose.yml -f docker/aedes.yml up
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
```
|
||||
|
||||
This will bring up the Mainflux docker services and interconnect them. This command can also be executed using the project's included Makefile:
|
||||
@@ -77,11 +77,7 @@ Additional details on using the CLI can be found in the [CLI documentation](http
|
||||
|
||||
## Documentation
|
||||
|
||||
Official documentation is hosted at [Mainflux Read The Docs page][docs]. Documentation is auto-generated from Markdown files in `./docs` directory. These can be viewed locally by [installing mkdocs](https://www.mkdocs.org/#installation) and running the command:
|
||||
|
||||
```bash
|
||||
mkdocs serve
|
||||
```
|
||||
Official documentation is hosted at [Mainflux Read The Docs page][docs]. Documentation is auto-generated, checkout the instructions on [official docs repository](https://github.com/mainflux/docs):
|
||||
|
||||
If you spot an error or a need for corrections, please let us know - or even better: send us a PR.
|
||||
|
||||
@@ -130,6 +126,8 @@ please head to company's [careers page][careers] or shoot us an e-mail at <caree
|
||||
|
||||
[Apache-2.0](LICENSE)
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fmainflux%2Fmainflux?ref=badge_large)
|
||||
|
||||
[banner]: https://github.com/mainflux/docs/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
|
||||
|
||||
@@ -4,15 +4,14 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"golang.org/x/net/context"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
context "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func issueEndpoint(svc authn.Service) endpoint.Endpoint {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
@@ -14,9 +15,9 @@ import (
|
||||
grpcapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -32,10 +33,10 @@ var svc authn.Service
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
|
||||
return authn.New(repo, idp, t)
|
||||
return authn.New(repo, uuidProvider, t)
|
||||
}
|
||||
|
||||
func startGRPCServer(svc authn.Service, port int) {
|
||||
@@ -46,39 +47,75 @@ func startGRPCServer(svc authn.Service, port int) {
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
|
||||
cases := map[string]struct {
|
||||
token string
|
||||
id string
|
||||
kind uint32
|
||||
err error
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
kind uint32
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
"issue for user with valid token": {"", email, authn.UserKey, nil},
|
||||
"issue for user that doesn't exist": {"", loginKey.Secret, 32, status.Error(codes.InvalidArgument, "received invalid token request")},
|
||||
{
|
||||
desc: "issue for user with valid token",
|
||||
id: email,
|
||||
kind: authn.UserKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
id: email,
|
||||
kind: authn.RecoveryKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue API key",
|
||||
id: userKey.Secret,
|
||||
kind: authn.APIKey,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "issue for invalid key type",
|
||||
id: email,
|
||||
kind: 32,
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
{
|
||||
desc: "issue for user that exist",
|
||||
id: "",
|
||||
kind: authn.APIKey,
|
||||
err: status.Error(codes.Unauthenticated, "unauthorized access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
for _, tc := range cases {
|
||||
_, err := client.Issue(context.Background(), &mainflux.IssueReq{Issuer: tc.id, Type: tc.kind})
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s", desc, tc.err, err))
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentify(t *testing.T) {
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
resetKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
|
||||
|
||||
userKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
recoveryKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing recovery key expected to succeed: %s", err))
|
||||
|
||||
apiKey, err := svc.Issue(context.Background(), userKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing API key expected to succeed: %s", err))
|
||||
|
||||
authAddr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, _ := grpc.Dial(authAddr, grpc.WithInsecure())
|
||||
client := grpcapi.NewClient(mocktracer.New(), conn, time.Second)
|
||||
@@ -88,36 +125,43 @@ func TestIdentify(t *testing.T) {
|
||||
token string
|
||||
id string
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{
|
||||
desc: "identify user with reset token",
|
||||
token: resetKey.Secret,
|
||||
desc: "identify user with recovery token",
|
||||
token: recoveryKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with user token",
|
||||
token: userKey.Secret,
|
||||
desc: "identify user with API token",
|
||||
token: apiKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
code: codes.OK,
|
||||
},
|
||||
{
|
||||
desc: "identify user with invalid login token",
|
||||
desc: "identify user with invalid user token",
|
||||
token: "invalid",
|
||||
id: "",
|
||||
err: status.Error(codes.Unauthenticated, "unauthorized access"),
|
||||
code: codes.Unauthenticated,
|
||||
},
|
||||
{
|
||||
desc: "identify user that doesn't exist",
|
||||
token: "",
|
||||
id: "",
|
||||
err: status.Error(codes.InvalidArgument, "received invalid token request"),
|
||||
code: codes.InvalidArgument,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
id, err := client.Identify(context.Background(), &mainflux.Token{Value: tc.token})
|
||||
assert.Equal(t, tc.id, id.GetValue(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.id, id.GetValue()))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.err, err))
|
||||
e, ok := status.FromError(err)
|
||||
assert.True(t, ok, "gRPC status can't be extracted from the error")
|
||||
assert.Equal(t, tc.code, e.Code(), fmt.Sprintf("%s: expected %s got %s", tc.desc, tc.code, e.Code()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
kitot "github.com/go-kit/kit/tracing/opentracing"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
mainflux "github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
@@ -74,12 +76,14 @@ func encodeIdentifyResponse(_ context.Context, grpcRes interface{}) (interface{}
|
||||
}
|
||||
|
||||
func encodeError(err error) error {
|
||||
switch err {
|
||||
case nil:
|
||||
switch {
|
||||
case errors.Contains(err, nil):
|
||||
return nil
|
||||
case authn.ErrMalformedEntity:
|
||||
case errors.Contains(err, authn.ErrMalformedEntity):
|
||||
return status.Error(codes.InvalidArgument, "received invalid token request")
|
||||
case authn.ErrUnauthorizedAccess, authn.ErrKeyExpired:
|
||||
case errors.Contains(err, authn.ErrUnauthorizedAccess):
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
case errors.Contains(err, authn.ErrKeyExpired):
|
||||
return status.Error(codes.Unauthenticated, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
httpapi "github.com/mainflux/mainflux/authn/api/http"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -63,9 +64,9 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
return authn.New(repo, idp, t)
|
||||
return authn.New(repo, uuidProvider, t)
|
||||
}
|
||||
|
||||
func newServer(svc authn.Service) *httptest.Server {
|
||||
@@ -80,16 +81,16 @@ func toJSON(data interface{}) string {
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
svc := newService()
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
|
||||
lk := issueRequest{Type: authn.UserKey}
|
||||
uk := issueRequest{Type: authn.UserKey}
|
||||
ak := issueRequest{Type: authn.APIKey, Duration: time.Hour}
|
||||
rk := issueRequest{Type: authn.RecoveryKey}
|
||||
uk := issueRequest{Type: authn.APIKey, Duration: time.Hour}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -99,48 +100,48 @@ func TestIssue(t *testing.T) {
|
||||
status int
|
||||
}{
|
||||
{
|
||||
desc: "issue login key",
|
||||
req: toJSON(lk),
|
||||
desc: "issue user key",
|
||||
req: toJSON(uk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
status: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
desc: "issue user key",
|
||||
req: toJSON(uk),
|
||||
desc: "issue API key",
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: loginKey.Secret,
|
||||
token: userKey.Secret,
|
||||
status: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
desc: "issue reset key",
|
||||
desc: "issue recovery key",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: loginKey.Secret,
|
||||
token: userKey.Secret,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
desc: "issue login key wrong content type",
|
||||
req: toJSON(lk),
|
||||
ct: "", token: loginKey.Secret,
|
||||
desc: "issue user key wrong content type",
|
||||
req: toJSON(uk),
|
||||
ct: "", token: userKey.Secret,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue key wrong content type",
|
||||
req: toJSON(rk),
|
||||
ct: "",
|
||||
token: loginKey.Secret,
|
||||
token: userKey.Secret,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "issue key unauthorized",
|
||||
req: toJSON(uk),
|
||||
req: toJSON(ak),
|
||||
ct: contentType,
|
||||
token: "wrong",
|
||||
status: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
desc: "issue reset key with empty token",
|
||||
desc: "issue recovery key with empty token",
|
||||
req: toJSON(rk),
|
||||
ct: contentType,
|
||||
token: "",
|
||||
@@ -238,12 +239,12 @@ func TestRetrieve(t *testing.T) {
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
svc := newService()
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
key := authn.Key{Type: authn.APIKey, IssuedAt: time.Now()}
|
||||
|
||||
k, err := svc.Issue(context.Background(), loginKey.Secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
k, err := svc.Issue(context.Background(), userKey.Secret, key)
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing user key expected to succeed: %s", err))
|
||||
|
||||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
@@ -258,13 +259,13 @@ func TestRevoke(t *testing.T) {
|
||||
{
|
||||
desc: "revoke an existing key",
|
||||
id: k.ID,
|
||||
token: loginKey.Secret,
|
||||
token: userKey.Secret,
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
desc: "revoke a non-existing key",
|
||||
id: "non-existing",
|
||||
token: loginKey.Secret,
|
||||
token: userKey.Secret,
|
||||
status: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -48,3 +48,7 @@ func (res revokeKeyRes) Headers() map[string]string {
|
||||
func (res revokeKeyRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
+17
-17
@@ -6,7 +6,6 @@ package http
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
@@ -67,7 +67,7 @@ func decodeIssue(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
issuer: r.Header.Get("Authorization"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(authn.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -100,28 +100,28 @@ func encodeResponse(_ context.Context, w http.ResponseWriter, response interface
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
switch err {
|
||||
case authn.ErrMalformedEntity:
|
||||
switch {
|
||||
case errors.Contains(err, authn.ErrMalformedEntity):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case authn.ErrUnauthorizedAccess:
|
||||
case errors.Contains(err, authn.ErrUnauthorizedAccess):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case authn.ErrNotFound:
|
||||
case errors.Contains(err, authn.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case authn.ErrConflict:
|
||||
case errors.Contains(err, authn.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case io.EOF, io.ErrUnexpectedEOF:
|
||||
case errors.Contains(err, io.EOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errUnsupportedContentType:
|
||||
case errors.Contains(err, io.ErrUnexpectedEOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, errUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
default:
|
||||
switch err.(type) {
|
||||
case *json.SyntaxError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case *json.UnmarshalTypeError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
errorVal, ok := err.(errors.Error)
|
||||
if ok {
|
||||
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -48,7 +49,7 @@ func TestIssue(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := tokenizer.Issue(tc.key)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +105,7 @@ func TestParse(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
key, err := tokenizer.Parse(tc.token)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s, got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.key, key, fmt.Sprintf("%s expected %v, got %v", tc.desc, tc.key, key))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
type claims struct {
|
||||
@@ -68,9 +69,9 @@ func (svc tokenizer) Parse(token string) (authn.Key, error) {
|
||||
if c.Type != nil && *c.Type == authn.APIKey {
|
||||
return c.toKey(), nil
|
||||
}
|
||||
return authn.Key{}, authn.ErrKeyExpired
|
||||
return authn.Key{}, errors.Wrap(authn.ErrKeyExpired, err)
|
||||
}
|
||||
return authn.Key{}, authn.ErrUnauthorizedAccess
|
||||
return authn.Key{}, errors.Wrap(authn.ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
return c.toKey(), nil
|
||||
|
||||
@@ -38,6 +38,9 @@ type Key struct {
|
||||
|
||||
// Expired verifies if the key is expired.
|
||||
func (k Key) Expired() bool {
|
||||
if k.Type == APIKey && k.ExpiresAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
return k.ExpiresAt.UTC().Before(time.Now().UTC())
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -37,12 +37,20 @@ func TestExpired(t *testing.T) {
|
||||
expired: true,
|
||||
},
|
||||
{
|
||||
desc: "key with no expiration date",
|
||||
desc: "user key with no expiration date",
|
||||
key: authn.Key{
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
expired: true,
|
||||
},
|
||||
{
|
||||
desc: "API key with no expiration date",
|
||||
key: authn.Key{
|
||||
IssuedAt: time.Now(),
|
||||
Type: authn.APIKey,
|
||||
},
|
||||
expired: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
var _ authn.IdentityProvider = (*identityProviderMock)(nil)
|
||||
|
||||
type identityProviderMock struct {
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
}
|
||||
|
||||
func (idp *identityProviderMock) ID() (string, error) {
|
||||
idp.mu.Lock()
|
||||
defer idp.mu.Unlock()
|
||||
|
||||
idp.counter++
|
||||
return fmt.Sprintf("%s%012d", "123e4567-e89b-12d3-a456-", idp.counter), nil
|
||||
}
|
||||
|
||||
// NewIdentityProvider creates "mirror" identity provider, i.e. generated
|
||||
// token will hold value provided by the caller.
|
||||
func NewIdentityProvider() authn.IdentityProvider {
|
||||
return &identityProviderMock{}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux authentication service
|
||||
description: HTTP API for managing platform API keys.
|
||||
version: "1.0.0"
|
||||
|
||||
paths:
|
||||
/keys:
|
||||
post:
|
||||
summary: Issue API key
|
||||
description: |
|
||||
Generates a new API key. Thew new API key will
|
||||
be uniquely identified by its ID.
|
||||
tags:
|
||||
- authn
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/KeyRequest"
|
||||
responses:
|
||||
201:
|
||||
description: Issued new key.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
409:
|
||||
description: Failed due to using already existing ID.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/keys/{id}:
|
||||
get:
|
||||
summary: Gets API key details.
|
||||
description: |
|
||||
Gets API key details for the given key.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/KeyRes"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Revoke API key
|
||||
description: |
|
||||
Revoke API key identified by the given ID.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ID"
|
||||
responses:
|
||||
204:
|
||||
description: Key revoked.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Key:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: API key unique identifier
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
issuer:
|
||||
type: string
|
||||
format: string
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key issuer
|
||||
secret:
|
||||
type: string
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZXhhbXBsZSIsImlhdCI6MTUxNjIzOTAyMn0.9UYAFWmPIn4ojss36LpIGSqABZHfADQmVuKQ4PJBMdI
|
||||
description: API Key value.
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the key is generated
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the Key expires
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: Login key secret (User's access token).
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ID:
|
||||
name: id
|
||||
description: API Key id.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
requestBodies:
|
||||
KeyRequest:
|
||||
description: JSON-formatted document describing key request.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
issuer:
|
||||
type: string
|
||||
format: e-mail
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key issuer
|
||||
duration:
|
||||
type: number
|
||||
format: integer
|
||||
example: 23456
|
||||
description: Number of seconds issued token is valid for.
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
KeyRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Key"
|
||||
+11
-5
@@ -7,8 +7,14 @@ import (
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
errSave = errors.New("failed to save key in database")
|
||||
errRetrieve = errors.New("failed to retrieve key from database")
|
||||
errDelete = errors.New("failed to delete key from database")
|
||||
)
|
||||
var _ authn.KeyRepository = (*repo)(nil)
|
||||
|
||||
const (
|
||||
@@ -37,11 +43,11 @@ func (kr repo) Save(ctx context.Context, key authn.Key) (string, error) {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok {
|
||||
if pqErr.Code.Name() == errDuplicate {
|
||||
return "", authn.ErrConflict
|
||||
return "", errors.Wrap(authn.ErrConflict, pqErr)
|
||||
}
|
||||
}
|
||||
|
||||
return "", err
|
||||
return "", errors.Wrap(errSave, err)
|
||||
}
|
||||
|
||||
return dbKey.ID, nil
|
||||
@@ -53,10 +59,10 @@ func (kr repo) Retrieve(ctx context.Context, issuer, id string) (authn.Key, erro
|
||||
if err := kr.db.QueryRowxContext(ctx, q, issuer, id).StructScan(&key); err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
|
||||
return authn.Key{}, authn.ErrNotFound
|
||||
return authn.Key{}, errors.Wrap(authn.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return authn.Key{}, err
|
||||
return authn.Key{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
return toKey(key), nil
|
||||
@@ -69,7 +75,7 @@ func (kr repo) Remove(ctx context.Context, issuer, id string) error {
|
||||
Issuer: issuer,
|
||||
}
|
||||
if _, err := kr.db.NamedExecContext(ctx, q, key); err != nil {
|
||||
return err
|
||||
return errors.Wrap(errDelete, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/authn/postgres"
|
||||
"github.com/mainflux/mainflux/authn/uuid"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -22,8 +23,7 @@ func TestKeySave(t *testing.T) {
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
idp := uuid.New()
|
||||
id, _ := idp.ID()
|
||||
id, _ := uuidProvider.New().ID()
|
||||
cases := []struct {
|
||||
desc string
|
||||
key authn.Key
|
||||
@@ -53,7 +53,7 @@ func TestKeySave(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Save(context.Background(), tc.key)
|
||||
assert.Equal(t, err, tc.err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,7 @@ func TestKeyRetrieve(t *testing.T) {
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
idp := uuid.New()
|
||||
id, _ := idp.ID()
|
||||
id, _ := uuidProvider.New().ID()
|
||||
key := authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
@@ -101,7 +100,7 @@ func TestKeyRetrieve(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := repo.Retrieve(context.Background(), tc.issuer, tc.id)
|
||||
assert.Equal(t, err, tc.err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +110,7 @@ func TestKeyRemove(t *testing.T) {
|
||||
|
||||
email := "user-save@example.com"
|
||||
expTime := time.Now().Add(5 * time.Minute)
|
||||
idp := uuid.New()
|
||||
id, _ := idp.ID()
|
||||
id, _ := uuidProvider.New().ID()
|
||||
key := authn.Key{
|
||||
Issuer: email,
|
||||
IssuedAt: time.Now(),
|
||||
@@ -143,6 +141,6 @@ func TestKeyRemove(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
err := repo.Remove(context.Background(), tc.issuer, tc.id)
|
||||
assert.Equal(t, err, tc.err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/authn/postgres"
|
||||
dockertest "gopkg.in/ory-am/dockertest.v3"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
const wrong string = "wrong-value"
|
||||
@@ -65,10 +65,11 @@ func TestMain(m *testing.M) {
|
||||
if db, err = postgres.Connect(dbConfig); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
+35
-39
@@ -5,14 +5,16 @@ package authn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
loginDuration = 10 * time.Hour
|
||||
resetDuration = 5 * time.Minute
|
||||
issuerName = "mainflux.authn"
|
||||
loginDuration = 10 * time.Hour
|
||||
recoveryDuration = 5 * time.Minute
|
||||
issuerName = "mainflux.authn"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -28,6 +30,12 @@ var (
|
||||
|
||||
// ErrConflict indicates that entity already exists.
|
||||
ErrConflict = errors.New("entity already exists")
|
||||
|
||||
errIssueUser = errors.New("failed to issue new user key")
|
||||
errIssueTmp = errors.New("failed to issue new temporary key")
|
||||
errRevoke = errors.New("failed to remove key")
|
||||
errRetrieve = errors.New("failed to retrieve key data")
|
||||
errIdentify = errors.New("failed to validate token")
|
||||
)
|
||||
|
||||
// Service specifies an API that must be fullfiled by the domain service
|
||||
@@ -53,17 +61,17 @@ type Service interface {
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
type service struct {
|
||||
keys KeyRepository
|
||||
idp IdentityProvider
|
||||
tokenizer Tokenizer
|
||||
keys KeyRepository
|
||||
uuidProvider mainflux.UUIDProvider
|
||||
tokenizer Tokenizer
|
||||
}
|
||||
|
||||
// New instantiates the auth service implementation.
|
||||
func New(keys KeyRepository, idp IdentityProvider, tokenizer Tokenizer) Service {
|
||||
func New(keys KeyRepository, up mainflux.UUIDProvider, tokenizer Tokenizer) Service {
|
||||
return &service{
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
idp: idp,
|
||||
tokenizer: tokenizer,
|
||||
keys: keys,
|
||||
uuidProvider: up,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,25 +83,27 @@ func (svc service) Issue(ctx context.Context, issuer string, key Key) (Key, erro
|
||||
case APIKey:
|
||||
return svc.userKey(ctx, issuer, key)
|
||||
case RecoveryKey:
|
||||
return svc.resetKey(ctx, issuer, key)
|
||||
return svc.tmpKey(issuer, recoveryDuration, key)
|
||||
default:
|
||||
return svc.loginKey(issuer, key)
|
||||
return svc.tmpKey(issuer, loginDuration, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) Revoke(ctx context.Context, issuer, id string) error {
|
||||
email, err := svc.login(issuer)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
|
||||
return svc.keys.Remove(ctx, email, id)
|
||||
if err := svc.keys.Remove(ctx, email, id); err != nil {
|
||||
return errors.Wrap(errRevoke, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc service) Retrieve(ctx context.Context, issuer, id string) (Key, error) {
|
||||
email, err := svc.login(issuer)
|
||||
if err != nil {
|
||||
return Key{}, err
|
||||
return Key{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
return svc.keys.Retrieve(ctx, email, id)
|
||||
@@ -102,7 +112,7 @@ func (svc service) Retrieve(ctx context.Context, issuer, id string) (Key, error)
|
||||
func (svc service) Identify(ctx context.Context, token string) (string, error) {
|
||||
c, err := svc.tokenizer.Parse(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", errors.Wrap(errIdentify, err)
|
||||
}
|
||||
|
||||
switch c.Type {
|
||||
@@ -127,27 +137,13 @@ func (svc service) Identify(ctx context.Context, token string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) loginKey(issuer string, key Key) (Key, error) {
|
||||
func (svc service) tmpKey(issuer string, duration time.Duration, key Key) (Key, error) {
|
||||
key.Secret = issuer
|
||||
return svc.tempKey(loginDuration, key)
|
||||
}
|
||||
|
||||
func (svc service) resetKey(ctx context.Context, issuer string, key Key) (Key, error) {
|
||||
issuer, err := svc.login(issuer)
|
||||
if err != nil {
|
||||
return Key{}, err
|
||||
}
|
||||
key.Secret = issuer
|
||||
|
||||
return svc.tempKey(resetDuration, key)
|
||||
}
|
||||
|
||||
func (svc service) tempKey(duration time.Duration, key Key) (Key, error) {
|
||||
key.Issuer = issuerName
|
||||
key.ExpiresAt = key.IssuedAt.Add(duration)
|
||||
val, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Key{}, err
|
||||
return Key{}, errors.Wrap(errIssueTmp, err)
|
||||
}
|
||||
|
||||
key.Secret = val
|
||||
@@ -157,24 +153,24 @@ func (svc service) tempKey(duration time.Duration, key Key) (Key, error) {
|
||||
func (svc service) userKey(ctx context.Context, issuer string, key Key) (Key, error) {
|
||||
email, err := svc.login(issuer)
|
||||
if err != nil {
|
||||
return Key{}, err
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.Issuer = email
|
||||
|
||||
id, err := svc.idp.ID()
|
||||
id, err := svc.uuidProvider.ID()
|
||||
if err != nil {
|
||||
return Key{}, err
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.ID = id
|
||||
|
||||
value, err := svc.tokenizer.Issue(key)
|
||||
if err != nil {
|
||||
return Key{}, err
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
key.Secret = value
|
||||
|
||||
if _, err := svc.keys.Save(ctx, key); err != nil {
|
||||
return Key{}, err
|
||||
return Key{}, errors.Wrap(errIssueUser, err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
|
||||
+23
-21
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/mocks"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -22,14 +24,14 @@ const (
|
||||
|
||||
func newService() authn.Service {
|
||||
repo := mocks.NewKeyRepository()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
uuidProvider := uuid.NewMock()
|
||||
t := jwt.New(secret)
|
||||
return authn.New(repo, idp, t)
|
||||
return authn.New(repo, uuidProvider, t)
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
svc := newService()
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
userKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
@@ -39,7 +41,7 @@ func TestIssue(t *testing.T) {
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue login key",
|
||||
desc: "issue user key",
|
||||
key: authn.Key{
|
||||
Type: authn.UserKey,
|
||||
IssuedAt: time.Now(),
|
||||
@@ -48,7 +50,7 @@ func TestIssue(t *testing.T) {
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue login key no issue time",
|
||||
desc: "issue user key no issue time",
|
||||
key: authn.Key{
|
||||
Type: authn.UserKey,
|
||||
},
|
||||
@@ -56,16 +58,16 @@ func TestIssue(t *testing.T) {
|
||||
err: authn.ErrInvalidKeyIssuedAt,
|
||||
},
|
||||
{
|
||||
desc: "issue user key",
|
||||
desc: "issue API key",
|
||||
key: authn.Key{
|
||||
Type: authn.APIKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
issuer: loginKey.Secret,
|
||||
issuer: userKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue user key unauthorized",
|
||||
desc: "issue API key unauthorized",
|
||||
key: authn.Key{
|
||||
Type: authn.APIKey,
|
||||
IssuedAt: time.Now(),
|
||||
@@ -74,35 +76,35 @@ func TestIssue(t *testing.T) {
|
||||
err: authn.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "issue user key no issue time",
|
||||
desc: "issue API key no issue time",
|
||||
key: authn.Key{
|
||||
Type: authn.APIKey,
|
||||
},
|
||||
issuer: loginKey.Secret,
|
||||
issuer: userKey.Secret,
|
||||
err: authn.ErrInvalidKeyIssuedAt,
|
||||
},
|
||||
{
|
||||
desc: "issue reset key",
|
||||
desc: "issue recovery key",
|
||||
key: authn.Key{
|
||||
Type: authn.RecoveryKey,
|
||||
IssuedAt: time.Now(),
|
||||
},
|
||||
issuer: loginKey.Secret,
|
||||
issuer: userKey.Secret,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue reset key no issue time",
|
||||
desc: "issue recovery key no issue time",
|
||||
key: authn.Key{
|
||||
Type: authn.RecoveryKey,
|
||||
},
|
||||
issuer: loginKey.Secret,
|
||||
issuer: userKey.Secret,
|
||||
err: authn.ErrInvalidKeyIssuedAt,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Issue(context.Background(), tc.issuer, tc.key)
|
||||
assert.Equal(t, err, tc.err, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
func TestRevoke(t *testing.T) {
|
||||
@@ -144,7 +146,7 @@ func TestRevoke(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.Revoke(context.Background(), tc.issuer, tc.id)
|
||||
assert.Equal(t, err, tc.err, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
func TestRetrieve(t *testing.T) {
|
||||
@@ -205,7 +207,7 @@ func TestRetrieve(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Retrieve(context.Background(), tc.issuer, tc.id)
|
||||
assert.Equal(t, err, tc.err, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
func TestIdentify(t *testing.T) {
|
||||
@@ -213,7 +215,7 @@ func TestIdentify(t *testing.T) {
|
||||
loginKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.UserKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
||||
|
||||
resetKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
|
||||
recoveryKey, err := svc.Issue(context.Background(), email, authn.Key{Type: authn.RecoveryKey, IssuedAt: time.Now()})
|
||||
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
|
||||
|
||||
userKey, err := svc.Issue(context.Background(), loginKey.Secret, authn.Key{Type: authn.APIKey, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
|
||||
@@ -239,8 +241,8 @@ func TestIdentify(t *testing.T) {
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "identify reset key",
|
||||
key: resetKey.Secret,
|
||||
desc: "identify recovery key",
|
||||
key: recoveryKey.Secret,
|
||||
id: email,
|
||||
err: nil,
|
||||
},
|
||||
@@ -272,7 +274,7 @@ func TestIdentify(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
id, err := svc.Identify(context.Background(), tc.key)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.id, id, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.id, id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: Mainflux authentication service
|
||||
description: HTTP API for managing platform API keys.
|
||||
version: "1.0.0"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
paths:
|
||||
/keys:
|
||||
post:
|
||||
summary: Issue API key
|
||||
description: |
|
||||
Generates a new API key. Thew new API key will
|
||||
be uniquely identified by its ID.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- name: key
|
||||
description: JSON-formatted document describing the new key.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/Key"
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
description: Issued new key.
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
409:
|
||||
description: Failed due to using already existing ID.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets API key details.
|
||||
description: |
|
||||
Gets API key details for the given key.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- name: id
|
||||
description: API Key id.
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
schema:
|
||||
$ref: "#/definitions/Key"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
summary: Revoke API key
|
||||
description: |
|
||||
Revoke API key identified by the given ID.
|
||||
tags:
|
||||
- authn
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- name: id
|
||||
description: API Key id.
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
204:
|
||||
description: Key revoked.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
|
||||
definitions:
|
||||
Key:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c5747f2f-2a7c-4fe1-b41a-51a5ae290945"
|
||||
description: API key unique identifier
|
||||
type:
|
||||
type: integer
|
||||
example: 0
|
||||
description: API key type. Keys of different type are processed differently
|
||||
issuer:
|
||||
type: string
|
||||
format: string
|
||||
example: "test@example.com"
|
||||
description: User's email or service identifier of API key issuer
|
||||
secret:
|
||||
type: string
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZXhhbXBsZSIsImlhdCI6MTUxNjIzOTAyMn0.9UYAFWmPIn4ojss36LpIGSqABZHfADQmVuKQ4PJBMdI
|
||||
description: API Key value.
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the key is generated
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2019-11-26 13:31:52"
|
||||
description: Time when the Key expires
|
||||
required:
|
||||
- type
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: Login key secret (User's access token).
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package uuid provides a UUID identity provider.
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/mainflux/mainflux/authn"
|
||||
)
|
||||
|
||||
var _ authn.IdentityProvider = (*uuidIdentityProvider)(nil)
|
||||
|
||||
type uuidIdentityProvider struct{}
|
||||
|
||||
// New instantiates a UUID identity provider.
|
||||
func New() authn.IdentityProvider {
|
||||
return &uuidIdentityProvider{}
|
||||
}
|
||||
|
||||
func (idp *uuidIdentityProvider) ID() (string, error) {
|
||||
id, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return id.String(), nil
|
||||
}
|
||||
+26
-5
@@ -55,7 +55,6 @@ The service is configured using the environment variables presented in the follo
|
||||
| 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 |
|
||||
@@ -64,7 +63,8 @@ The service is configured using the environment variables presented in the follo
|
||||
| MF_BOOTSTRAP_ES_DB | Bootstrap service event source database | 0 |
|
||||
| MF_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
| MF_BOOTSTRAP_THINGS_TIMEOUT | Things gRPC request timeout in seconds | 1 |
|
||||
| MF_AUTHN_GRPC_URL | AuthN service gRPC URL | localhost:8181 |
|
||||
| MF_AUTHN_GRPC_TIMEOUT | AuthN service gRPC request timeout in seconds | 1s |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -101,7 +101,6 @@ version: "2"
|
||||
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]
|
||||
@@ -110,7 +109,8 @@ version: "2"
|
||||
MF_BOOTSTRAP_ES_DB: [Bootstrap service event source database]
|
||||
MF_BOOTSTRAP_EVENT_CONSUMER: [Bootstrap service event source consumer name]
|
||||
MF_JAEGER_URL: [Jaeger server URL]
|
||||
MF_BOOTSTRAP_THINGS_TIMEOUT: [Things gRPC request timeout in seconds]
|
||||
MF_AUTHN_GRPC_URL: [AuthN service gRPC URL]
|
||||
MF_AUTHN_GRPC_TIMEOUT: [AuthN service gRPC request timeout in seconds]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
@@ -128,7 +128,28 @@ make bootstrap
|
||||
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_ENCRYPT_KEY=[Hex-encoded encryption key used for secure bootstrap] 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] MF_JAEGER_URL=[Jaeger server URL] MF_BOOTSTRAP_THINGS_TIMEOUT=[Things gRPC request timeout in seconds] $GOBIN/mainflux-bootstrap
|
||||
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_ENCRYPT_KEY=[Hex-encoded encryption key used for secure bootstrap] \
|
||||
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_JAEGER_URL=[Jaeger server URL] \
|
||||
MF_AUTHN_GRPC_URL=[AuthN service gRPC URL] \
|
||||
MF_AUTHN_GRPC_TIMEOUT=[AuthN service gRPC request timeout in seconds] \
|
||||
$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.
|
||||
|
||||
+28
-40
@@ -34,7 +34,7 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
saved, err := svc.Add(req.key, config)
|
||||
saved, err := svc.Add(req.token, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -162,49 +162,37 @@ func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
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,
|
||||
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,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
default:
|
||||
res := listRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
Configs: []viewRes{},
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+73
-160
@@ -22,7 +22,7 @@ import (
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
bsapi "github.com/mainflux/mainflux/bootstrap/api"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
@@ -78,6 +78,12 @@ var (
|
||||
ClientKey: "newkey",
|
||||
CACert: "newca",
|
||||
}
|
||||
|
||||
bsErrorRes = toJSON(errorRes{bootstrap.ErrBootstrap.Error()})
|
||||
unauthRes = toJSON(errorRes{bootstrap.ErrUnauthorizedAccess.Error()})
|
||||
malformedRes = toJSON(errorRes{bootstrap.ErrMalformedEntity.Error()})
|
||||
extKeyNotFoundRes = toJSON(errorRes{bootstrap.ErrExternalKeyNotFound.Error()})
|
||||
extSecKeyNotFoundRes = toJSON(errorRes{bootstrap.ErrSecureBootstrap.Error()})
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
@@ -150,8 +156,8 @@ func dec(in []byte) ([]byte, error) {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func newService(authn mainflux.AuthNServiceClient, unknown map[string]string, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository(unknown)
|
||||
func newService(authn mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
@@ -196,7 +202,7 @@ func TestAdd(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
data := toJSON(addReq)
|
||||
@@ -320,7 +326,7 @@ func TestView(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
c := newConfig([]bootstrap.Channel{})
|
||||
|
||||
@@ -417,7 +423,7 @@ func TestUpdate(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -511,7 +517,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -606,7 +612,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -718,7 +724,7 @@ func TestList(t *testing.T) {
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
path := fmt.Sprintf("%s/%s", bs.URL, "things/configs")
|
||||
|
||||
@@ -967,7 +973,7 @@ func TestRemove(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -1025,108 +1031,11 @@ func TestRemove(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUnknown(t *testing.T) {
|
||||
unknownNum := 10
|
||||
unknown := make([]config, unknownNum)
|
||||
unknownConfigs := make(map[string]string, unknownNum)
|
||||
// Save some unknown elements.
|
||||
for i := 0; i < unknownNum; i++ {
|
||||
u := config{
|
||||
ExternalID: fmt.Sprintf("key-%s", strconv.Itoa(i)),
|
||||
ExternalKey: fmt.Sprintf("%s%s", addExternalKey, strconv.Itoa(i)),
|
||||
}
|
||||
unknownConfigs[u.ExternalID] = u.ExternalKey
|
||||
unknown[i] = u
|
||||
}
|
||||
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, unknownConfigs, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
path := fmt.Sprintf("%s/%s", bs.URL, "things/unknown/configs")
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
auth string
|
||||
url string
|
||||
status int
|
||||
res []config
|
||||
}{
|
||||
{
|
||||
desc: "view unknown unauthorized",
|
||||
auth: invalidToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 5),
|
||||
status: http.StatusForbidden,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with an empty token",
|
||||
auth: "",
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 5),
|
||||
status: http.StatusForbidden,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with limit < 0",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, -5),
|
||||
status: http.StatusBadRequest,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with offset < 0",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, -3, 5),
|
||||
status: http.StatusBadRequest,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view unknown with invalid query params",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d&key=%%", path, 0, -5),
|
||||
status: http.StatusBadRequest,
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "view a list of unknown",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d", path, 0, 5),
|
||||
status: http.StatusOK,
|
||||
res: unknown[:5],
|
||||
},
|
||||
{
|
||||
desc: "view unknown with no page paremeters",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s", path),
|
||||
status: http.StatusOK,
|
||||
res: unknown[:10],
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := testRequest{
|
||||
client: bs.Client(),
|
||||
method: http.MethodGet,
|
||||
url: tc.url,
|
||||
token: tc.auth,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
var body map[string][]config
|
||||
|
||||
json.NewDecoder(res.Body).Decode(&body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
assert.ElementsMatch(t, tc.res, body["configs"], fmt.Sprintf("%s: expected response '%s' got '%s'", tc.desc, tc.res, body["configs"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, map[string]string{}, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -1163,68 +1072,68 @@ func TestBootstrap(t *testing.T) {
|
||||
data := toJSON(s)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
external_id string
|
||||
external_key string
|
||||
status int
|
||||
res string
|
||||
secure bool
|
||||
desc string
|
||||
externalID string
|
||||
externalKey string
|
||||
status int
|
||||
res string
|
||||
secure bool
|
||||
}{
|
||||
{
|
||||
desc: "bootstrap a Thing with unknown ID",
|
||||
external_id: unknown,
|
||||
external_key: c.ExternalKey,
|
||||
status: http.StatusNotFound,
|
||||
res: "",
|
||||
secure: false,
|
||||
desc: "bootstrap a Thing with unknown ID",
|
||||
externalID: unknown,
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusNotFound,
|
||||
res: bsErrorRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Thing with an empty ID",
|
||||
external_id: "",
|
||||
external_key: c.ExternalKey,
|
||||
status: http.StatusBadRequest,
|
||||
res: "",
|
||||
secure: false,
|
||||
desc: "bootstrap a Thing with an empty ID",
|
||||
externalID: "",
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusBadRequest,
|
||||
res: malformedRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Thing with unknown key",
|
||||
external_id: c.ExternalID,
|
||||
external_key: unknown,
|
||||
status: http.StatusNotFound,
|
||||
res: "",
|
||||
secure: false,
|
||||
desc: "bootstrap a Thing with unknown key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: unknown,
|
||||
status: http.StatusNotFound,
|
||||
res: extKeyNotFoundRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Thing with an empty key",
|
||||
external_id: c.ExternalID,
|
||||
external_key: "",
|
||||
status: http.StatusForbidden,
|
||||
res: "",
|
||||
secure: false,
|
||||
desc: "bootstrap a Thing with an empty key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: "",
|
||||
status: http.StatusForbidden,
|
||||
res: unauthRes,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap known Thing",
|
||||
external_id: c.ExternalID,
|
||||
external_key: c.ExternalKey,
|
||||
status: http.StatusOK,
|
||||
res: data,
|
||||
secure: false,
|
||||
desc: "bootstrap known Thing",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusOK,
|
||||
res: data,
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap secure",
|
||||
external_id: fmt.Sprintf("secure/%s", c.ExternalID),
|
||||
external_key: hex.EncodeToString(encExternKey),
|
||||
status: http.StatusOK,
|
||||
res: data,
|
||||
secure: true,
|
||||
desc: "bootstrap secure",
|
||||
externalID: fmt.Sprintf("secure/%s", c.ExternalID),
|
||||
externalKey: hex.EncodeToString(encExternKey),
|
||||
status: http.StatusOK,
|
||||
res: data,
|
||||
secure: true,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap secure with unencrypted key",
|
||||
external_id: fmt.Sprintf("secure/%s", c.ExternalID),
|
||||
external_key: c.ExternalKey,
|
||||
status: http.StatusNotFound,
|
||||
res: "",
|
||||
secure: true,
|
||||
desc: "bootstrap secure with unencrypted key",
|
||||
externalID: fmt.Sprintf("secure/%s", c.ExternalID),
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusNotFound,
|
||||
res: extSecKeyNotFoundRes,
|
||||
secure: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1232,8 +1141,8 @@ func TestBootstrap(t *testing.T) {
|
||||
req := testRequest{
|
||||
client: bs.Client(),
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/things/bootstrap/%s", bs.URL, tc.external_id),
|
||||
token: tc.external_key,
|
||||
url: fmt.Sprintf("%s/things/bootstrap/%s", bs.URL, tc.externalID),
|
||||
token: tc.externalKey,
|
||||
}
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
@@ -1241,7 +1150,7 @@ func TestBootstrap(t *testing.T) {
|
||||
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
if tc.secure {
|
||||
if tc.secure && tc.status == http.StatusOK {
|
||||
body, err = dec(body)
|
||||
}
|
||||
|
||||
@@ -1254,7 +1163,7 @@ func TestChangeState(t *testing.T) {
|
||||
users := mocks.NewUsersService(map[string]string{validToken: email})
|
||||
|
||||
ts := newThingsServer(newThingsService(users))
|
||||
svc := newService(users, nil, ts.URL)
|
||||
svc := newService(users, ts.URL)
|
||||
bs := newBootstrapServer(svc)
|
||||
|
||||
c := newConfig([]bootstrap.Channel{bootstrap.Channel{ID: "1"}})
|
||||
@@ -1377,3 +1286,7 @@ type configPage struct {
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []config `json:"configs"`
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
+23
-23
@@ -25,9 +25,9 @@ func NewLoggingMiddleware(svc bootstrap.Service, logger log.Logger) bootstrap.Se
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Add(key string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
func (lm *loggingMiddleware) Add(token string, cfg 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))
|
||||
message := fmt.Sprintf("Method add for token %s and thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -35,12 +35,12 @@ func (lm *loggingMiddleware) Add(key string, cfg bootstrap.Config) (saved bootst
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Add(key, cfg)
|
||||
return lm.svc.Add(token, cfg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) View(key, id string) (saved bootstrap.Config, err error) {
|
||||
func (lm *loggingMiddleware) View(token, 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))
|
||||
message := fmt.Sprintf("Method view for token %s and thing %s took %s to complete", token, saved.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -48,12 +48,12 @@ func (lm *loggingMiddleware) View(key, id string) (saved bootstrap.Config, err e
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.View(key, id)
|
||||
return lm.svc.View(token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Update(key string, cfg bootstrap.Config) (err error) {
|
||||
func (lm *loggingMiddleware) Update(token string, cfg 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, cfg.MFThing, time.Since(begin))
|
||||
message := fmt.Sprintf("Method update for token %s and thing %s took %s to complete", token, cfg.MFThing, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -61,10 +61,10 @@ func (lm *loggingMiddleware) Update(key string, cfg bootstrap.Config) (err error
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Update(key, cfg)
|
||||
return lm.svc.Update(token, cfg)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateCert(key, thingID, clientCert, clientKey, caCert string) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateCert(token, thingID, clientCert, clientKey, caCert string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method update_cert for thing with id %s took %s to complete", thingID, time.Since(begin))
|
||||
if err != nil {
|
||||
@@ -74,12 +74,12 @@ func (lm *loggingMiddleware) UpdateCert(key, thingID, clientCert, clientKey, caC
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateCert(key, thingID, clientCert, clientKey, caCert)
|
||||
return lm.svc.UpdateCert(token, thingID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateConnections(key, id string, connections []string) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateConnections(token, 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))
|
||||
message := fmt.Sprintf("Method update_connections for token %s and thing %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -87,12 +87,12 @@ func (lm *loggingMiddleware) UpdateConnections(key, id string, connections []str
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateConnections(key, id, connections)
|
||||
return lm.svc.UpdateConnections(token, id, connections)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) List(key string, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
func (lm *loggingMiddleware) List(token 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))
|
||||
message := fmt.Sprintf("Method list for token %s and offset %d and limit %d took %s to complete", token, offset, limit, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -100,12 +100,12 @@ func (lm *loggingMiddleware) List(key string, filter bootstrap.Filter, offset, l
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.List(key, filter, offset, limit)
|
||||
return lm.svc.List(token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Remove(key, id string) (err error) {
|
||||
func (lm *loggingMiddleware) Remove(token, 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))
|
||||
message := fmt.Sprintf("Method remove for token %s and thing %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -113,7 +113,7 @@ func (lm *loggingMiddleware) Remove(key, id string) (err error) {
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Remove(key, id)
|
||||
return lm.svc.Remove(token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
@@ -129,9 +129,9 @@ func (lm *loggingMiddleware) Bootstrap(externalKey, externalID string, secure bo
|
||||
return lm.svc.Bootstrap(externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ChangeState(key, id string, state bootstrap.State) (err error) {
|
||||
func (lm *loggingMiddleware) ChangeState(token, 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))
|
||||
message := fmt.Sprintf("Method change_state for token %s and thing %s took %s to complete", token, id, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
@@ -139,7 +139,7 @@ func (lm *loggingMiddleware) ChangeState(key, id string, state bootstrap.State)
|
||||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ChangeState(key, id, state)
|
||||
return lm.svc.ChangeState(token, id, state)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
|
||||
+16
-16
@@ -30,67 +30,67 @@ func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency m
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Add(key string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
func (mm *metricsMiddleware) Add(token string, cfg 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, cfg)
|
||||
return mm.svc.Add(token, cfg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) View(id, key string) (saved bootstrap.Config, err error) {
|
||||
func (mm *metricsMiddleware) View(token, id 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)
|
||||
return mm.svc.View(token, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Update(key string, cfg bootstrap.Config) (err error) {
|
||||
func (mm *metricsMiddleware) Update(token string, cfg 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, cfg)
|
||||
return mm.svc.Update(token, cfg)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateCert(key, thingKey, clientCert, clientKey, caCert string) (err error) {
|
||||
func (mm *metricsMiddleware) UpdateCert(token, thingKey, clientCert, clientKey, caCert string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_cert").Add(1)
|
||||
mm.latency.With("method", "update_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateCert(key, thingKey, clientCert, clientKey, caCert)
|
||||
return mm.svc.UpdateCert(token, thingKey, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateConnections(key, id string, connections []string) (err error) {
|
||||
func (mm *metricsMiddleware) UpdateConnections(token, 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)
|
||||
return mm.svc.UpdateConnections(token, id, connections)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) List(key string, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
func (mm *metricsMiddleware) List(token 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)
|
||||
return mm.svc.List(token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Remove(id, key string) (err error) {
|
||||
func (mm *metricsMiddleware) Remove(token, id 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)
|
||||
return mm.svc.Remove(token, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) Bootstrap(externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
@@ -102,13 +102,13 @@ func (mm *metricsMiddleware) Bootstrap(externalKey, externalID string, secure bo
|
||||
return mm.svc.Bootstrap(externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ChangeState(id, key string, state bootstrap.State) (err error) {
|
||||
func (mm *metricsMiddleware) ChangeState(token, id 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)
|
||||
return mm.svc.ChangeState(token, id, state)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(channel bootstrap.Channel) (err error) {
|
||||
|
||||
@@ -10,7 +10,7 @@ type apiReq interface {
|
||||
}
|
||||
|
||||
type addReq struct {
|
||||
key string
|
||||
token string
|
||||
ThingID string `json:"thing_id"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
@@ -23,7 +23,7 @@ type addReq struct {
|
||||
}
|
||||
|
||||
func (req addReq) validate() error {
|
||||
if req.key == "" {
|
||||
if req.token == "" {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
|
||||
@@ -11,28 +11,28 @@ import (
|
||||
func TestAddReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
token string
|
||||
externalID string
|
||||
externalKey string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty key",
|
||||
key: "",
|
||||
token: "",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "empty external ID",
|
||||
key: "key",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "external-key",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
key: "key",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "",
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
@@ -41,7 +41,7 @@ func TestAddReqValidation(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
req := addReq{
|
||||
key: tc.key,
|
||||
token: tc.token,
|
||||
ExternalID: tc.externalID,
|
||||
ExternalKey: tc.externalKey,
|
||||
}
|
||||
|
||||
@@ -89,27 +89,6 @@ 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"`
|
||||
@@ -142,3 +121,7 @@ func (res stateRes) Headers() map[string]string {
|
||||
func (res stateRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type errorRes struct {
|
||||
Err string `json:"error"`
|
||||
}
|
||||
|
||||
+38
-57
@@ -6,7 +6,6 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
@@ -29,6 +29,8 @@ const (
|
||||
var (
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
errInvalidQueryParams = errors.New("invalid query params")
|
||||
errInvalidLimitParam = errors.New("invalid limit query param")
|
||||
errInvalidOffsetParam = errors.New("invalid offset query param")
|
||||
fullMatch = []string{"state", "external_id", "mainflux_id", "mainflux_key"}
|
||||
partialMatch = []string{"name"}
|
||||
)
|
||||
@@ -76,12 +78,6 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader) http.Hand
|
||||
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, false),
|
||||
decodeBootstrapRequest,
|
||||
@@ -117,9 +113,9 @@ func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addReq{key: r.Header.Get("Authorization")}
|
||||
req := addReq{token: r.Header.Get("Authorization")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -133,7 +129,7 @@ func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error
|
||||
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 nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -150,7 +146,7 @@ func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, e
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -164,28 +160,7 @@ func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, e
|
||||
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 nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -231,7 +206,7 @@ func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error)
|
||||
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 nil, errors.Wrap(bootstrap.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
@@ -275,32 +250,38 @@ func encodeSecureRes(_ context.Context, w http.ResponseWriter, response interfac
|
||||
}
|
||||
|
||||
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:
|
||||
switch errorVal := err.(type) {
|
||||
case errors.Error:
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
switch {
|
||||
case errors.Contains(errorVal, errUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case errors.Contains(errorVal, errInvalidQueryParams):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case *json.UnmarshalTypeError:
|
||||
case errors.Contains(errorVal, bootstrap.ErrMalformedEntity):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(errorVal, bootstrap.ErrNotFound):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case errors.Contains(errorVal, bootstrap.ErrUnauthorizedAccess):
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case errors.Contains(errorVal, bootstrap.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
case errors.Contains(errorVal, bootstrap.ErrThings):
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
case errors.Contains(errorVal, io.EOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(errorVal, io.ErrUnexpectedEOF):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
if errorVal.Msg() != "" {
|
||||
if err := json.NewEncoder(w).Encode(errorRes{Err: errorVal.Msg()}); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,13 +302,13 @@ func parsePagePrams(q url.Values) (uint64, uint64, error) {
|
||||
offset, err := parseUint(q.Get("offset"))
|
||||
q.Del("offset")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
return 0, 0, errors.Wrap(errInvalidOffsetParam, err)
|
||||
}
|
||||
|
||||
limit, err := parseUint(q.Get("limit"))
|
||||
q.Del("limit")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
return 0, 0, errors.Wrap(errInvalidLimitParam, err)
|
||||
}
|
||||
|
||||
if limit > maxLimit {
|
||||
|
||||
+15
-22
@@ -32,7 +32,6 @@ type Channel struct {
|
||||
|
||||
// Filter is used for the search filters.
|
||||
type Filter struct {
|
||||
Unknown bool
|
||||
FullMatch map[string]string
|
||||
PartialMatch map[string]string
|
||||
}
|
||||
@@ -50,60 +49,54 @@ type ConfigsPage struct {
|
||||
type ConfigRepository interface {
|
||||
// Save persists the Config. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
Save(Config, []string) (string, error)
|
||||
Save(cfg Config, chsConnIDs []string) (string, error)
|
||||
|
||||
// RetrieveByID retrieves the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
RetrieveByID(string, string) (Config, error)
|
||||
RetrieveByID(owner, id 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
|
||||
RetrieveAll(owner string, filter Filter, offset, limit uint64) ConfigsPage
|
||||
|
||||
// RetrieveByExternalID returns Config for given external ID.
|
||||
RetrieveByExternalID(string) (Config, error)
|
||||
RetrieveByExternalID(externalID string) (Config, error)
|
||||
|
||||
// Update updates an existing Config. A non-nil error is returned
|
||||
// to indicate operation failure.
|
||||
Update(Config) error
|
||||
Update(cfg Config) error
|
||||
|
||||
// UpdateCerts updates an existing Config certificate and key.
|
||||
// UpdateCerts updates an existing Config certificate and owner.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(string, string, string, string, string) error
|
||||
UpdateCert(owner, thingID, clientCert, clientKey, caCert string) error
|
||||
|
||||
// UpdateConnections updates a list of Channels the Config is connected to
|
||||
// adding new Channels if needed.
|
||||
UpdateConnections(string, string, []Channel, []string) error
|
||||
UpdateConnections(owner, id string, channels []Channel, connections []string) error
|
||||
|
||||
// Remove removes the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(string, string) error
|
||||
Remove(owner, id 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
|
||||
ChangeState(owner, id string, state State) error
|
||||
|
||||
// ListExisting retrieves those channels from the given list that exist in DB.
|
||||
ListExisting(string, []string) ([]Channel, error)
|
||||
ListExisting(owner string, ids []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
|
||||
RemoveThing(id string) error
|
||||
|
||||
// UpdateChannel updates channel with the given ID.
|
||||
UpdateChannel(Channel) error
|
||||
UpdateChannel(c Channel) error
|
||||
|
||||
// RemoveChannel removes channel with the given ID.
|
||||
RemoveChannel(string) error
|
||||
RemoveChannel(id string) error
|
||||
|
||||
// DisconnectHandler changes state of the Config when the corresponding Thing is
|
||||
// disconnected from the Channel.
|
||||
DisconnectThing(string, string) error
|
||||
DisconnectThing(channelID, thingID string) error
|
||||
}
|
||||
|
||||
+11
-67
@@ -24,15 +24,13 @@ type configRepositoryMock struct {
|
||||
counter uint64
|
||||
configs map[string]bootstrap.Config
|
||||
channels map[string]bootstrap.Channel
|
||||
unknown map[string]string
|
||||
}
|
||||
|
||||
// NewConfigsRepository creates in-memory config repository.
|
||||
func NewConfigsRepository(unknown map[string]string) bootstrap.ConfigRepository {
|
||||
func NewConfigsRepository() bootstrap.ConfigRepository {
|
||||
return &configRepositoryMock{
|
||||
configs: make(map[string]bootstrap.Config),
|
||||
channels: make(map[string]bootstrap.Channel),
|
||||
unknown: unknown,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,12 +59,11 @@ func (crm *configRepositoryMock) Save(config bootstrap.Config, connections []str
|
||||
}
|
||||
|
||||
crm.configs[config.MFThing] = config
|
||||
delete(crm.unknown, config.ExternalID)
|
||||
|
||||
return config.MFThing, nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveByID(key, id string) (bootstrap.Config, error) {
|
||||
func (crm *configRepositoryMock) RetrieveByID(token, id string) (bootstrap.Config, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
@@ -74,7 +71,7 @@ func (crm *configRepositoryMock) RetrieveByID(key, id string) (bootstrap.Config,
|
||||
if !ok {
|
||||
return bootstrap.Config{}, bootstrap.ErrNotFound
|
||||
}
|
||||
if c.Owner != key {
|
||||
if c.Owner != token {
|
||||
return bootstrap.Config{}, bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
@@ -82,7 +79,7 @@ func (crm *configRepositoryMock) RetrieveByID(key, id string) (bootstrap.Config,
|
||||
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) RetrieveAll(key string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
func (crm *configRepositoryMock) RetrieveAll(token string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
@@ -110,7 +107,7 @@ func (crm *configRepositoryMock) RetrieveAll(key string, filter bootstrap.Filter
|
||||
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 {
|
||||
v.Owner == token {
|
||||
if id >= first && id < last {
|
||||
configs = append(configs, v)
|
||||
}
|
||||
@@ -180,7 +177,7 @@ func (crm *configRepositoryMock) UpdateCert(owner, thingID, clientCert, clientKe
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) UpdateConnections(key, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
func (crm *configRepositoryMock) UpdateConnections(token, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
@@ -206,12 +203,12 @@ func (crm *configRepositoryMock) UpdateConnections(key, id string, channels []bo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) Remove(key, id string) error {
|
||||
func (crm *configRepositoryMock) Remove(token, id string) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
for k, v := range crm.configs {
|
||||
if v.Owner == key && k == id {
|
||||
if v.Owner == token && k == id {
|
||||
delete(crm.configs, k)
|
||||
break
|
||||
}
|
||||
@@ -220,7 +217,7 @@ func (crm *configRepositoryMock) Remove(key, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crm *configRepositoryMock) ChangeState(key, id string, state bootstrap.State) error {
|
||||
func (crm *configRepositoryMock) ChangeState(token, id string, state bootstrap.State) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
@@ -228,7 +225,7 @@ func (crm *configRepositoryMock) ChangeState(key, id string, state bootstrap.Sta
|
||||
if !ok {
|
||||
return bootstrap.ErrNotFound
|
||||
}
|
||||
if config.Owner != key {
|
||||
if config.Owner != token {
|
||||
return bootstrap.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
@@ -237,60 +234,7 @@ func (crm *configRepositoryMock) ChangeState(key, id string, state bootstrap.Sta
|
||||
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) {
|
||||
func (crm *configRepositoryMock) ListExisting(token string, connections []string) ([]bootstrap.Channel, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
|
||||
@@ -167,15 +167,15 @@ func (svc *mainfluxThings) UpdateKey(context.Context, string, string, string) er
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListThings(context.Context, string, uint64, uint64, string, things.Metadata) (things.ThingsPage, error) {
|
||||
func (svc *mainfluxThings) ListThings(context.Context, string, uint64, uint64, string, things.Metadata) (things.Page, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListChannelsByThing(context.Context, string, string, uint64, uint64) (things.ChannelsPage, error) {
|
||||
func (svc *mainfluxThings) ListChannelsByThing(context.Context, string, string, uint64, uint64, bool) (things.ChannelsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListThingsByChannel(context.Context, string, string, uint64, uint64) (things.ThingsPage, error) {
|
||||
func (svc *mainfluxThings) ListThingsByChannel(context.Context, string, string, uint64, uint64, bool) (things.Page, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux Bootstrap service
|
||||
description: HTTP API for managing platform things configuration.
|
||||
version: "1.0.0"
|
||||
|
||||
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: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCreateReq"
|
||||
responses:
|
||||
201:
|
||||
$ref: "#/components/responses/ConfigCreateRes"
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
415:
|
||||
description: Missing or invalid content type.
|
||||
500:
|
||||
$ref: "#/components/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: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/State"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/ConfigListRes"
|
||||
400:
|
||||
description: Failed due to malformed query parameters.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/{configId}:
|
||||
get:
|
||||
summary: Retrieves config info (with channels).
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/ConfigRes"
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Config does not exist.
|
||||
500:
|
||||
$ref: "#/components/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: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigUpdateReq"
|
||||
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: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
summary: Removes a Config
|
||||
description: |
|
||||
Removes a Config. In case of successful removal the service will ensure
|
||||
that the removed config is disconnected from all of the Mainflux channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
204:
|
||||
description: Config removed.
|
||||
400:
|
||||
description: Failed due to malformed config ID.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/configs/certs/{configId}:
|
||||
patch:
|
||||
summary: Updates certs
|
||||
description: |
|
||||
Update is performed by replacing the current certificate data with values
|
||||
provided in a request payload.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCertUpdateReq"
|
||||
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: "#/components/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: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigConnUpdateReq"
|
||||
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: "#/components/responses/ServiceError"
|
||||
/things/bootstrap/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and external key.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ConfigAuth"
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/things/bootstrap/secure/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and encrypted external key.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/EncConfigAuth"
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
500:
|
||||
$ref: "#/components/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: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/ConfigStateUpdateReq'
|
||||
responses:
|
||||
204:
|
||||
description: Config removed.
|
||||
400:
|
||||
description: Failed due to malformed config's ID.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
State:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
Config:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_id:
|
||||
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 unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
state:
|
||||
$ref: "#/components/schemas/State"
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
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: "#/components/schemas/Config"
|
||||
required:
|
||||
- configs
|
||||
BootstrapConfig:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_id:
|
||||
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.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
ca_cert:
|
||||
type: string
|
||||
description: Issuing CA certificate.
|
||||
required:
|
||||
- mainflux_id
|
||||
- mainflux_key
|
||||
- mainflux_channels
|
||||
- content
|
||||
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ConfigAuth:
|
||||
name: configAuthorization
|
||||
description: Configuration external key.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
EncConfigAuth:
|
||||
name: configAuthorization
|
||||
description: |
|
||||
Hex-encoded configuration external key encrypted using
|
||||
the AES algorithm and SHA256 sum of the external key
|
||||
itself as an encryption key.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ConfigId:
|
||||
name: configId
|
||||
description: Unique Config identifier. It's the ID of the corresponding Thing.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
ExternalId:
|
||||
name: externalId
|
||||
description: Unique Config identifier provided by external entity.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
State:
|
||||
name: state
|
||||
description: A state of items
|
||||
in: query
|
||||
schema:
|
||||
$ref: "#/components/schemas/State"
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Name of the config. Search by name is partial-match and case-insensitive.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
ConfigCreateReq:
|
||||
description: JSON-formatted document describing the new config.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique 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:
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- name
|
||||
ConfigCertUpdateReq:
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
client_cert:
|
||||
type: string
|
||||
client_key:
|
||||
type: string
|
||||
ca_cert:
|
||||
type: string
|
||||
ConfigConnUpdateReq:
|
||||
description: Array if IDs the thing is be connected to.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
ConfigStateUpdateReq:
|
||||
description: Update the state of the Config.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
state:
|
||||
$ref: "#/components/schemas/State"
|
||||
|
||||
responses:
|
||||
ConfigCreateRes:
|
||||
description: Config registered.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created configuration's relative URL (i.e. /things/configs/{configId}).
|
||||
ConfigListRes:
|
||||
description: Data retrieved. Configs from this list don't contain channels.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConfigList"
|
||||
ConfigRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Config"
|
||||
BootstrapConfigRes:
|
||||
description: |
|
||||
Data retrieved. If secure, a response is encrypted using
|
||||
the secret key, so the response is in the binary form.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BootstrapConfig"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
+85
-122
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,6 +28,20 @@ const (
|
||||
SELECT channel_id FROM connections c WHERE ch.mainflux_channel = c.channel_id);`
|
||||
)
|
||||
|
||||
var (
|
||||
errSaveDB = errors.New("failed to save bootstrap configuration to database")
|
||||
errMarshalChannel = errors.New("failed to marshal channel into json")
|
||||
errUnmarshalChannel = errors.New("failed to unmarshal json to channel")
|
||||
errSaveChannels = errors.New("failed to insert channels to database")
|
||||
errSaveConnections = errors.New("failed to insert connections to database")
|
||||
errRetrieve = errors.New("failed to retreive bootstrap configuration from database")
|
||||
errUpdate = errors.New("failed to update bootstrap configuration in database")
|
||||
errRemove = errors.New("failed to remove bootstrap configuration from database")
|
||||
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
|
||||
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
|
||||
errDisconnectThing = errors.New("failed to disconnect thing in bootstrap configuration in database")
|
||||
)
|
||||
|
||||
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
|
||||
|
||||
type configRepository struct {
|
||||
@@ -40,13 +55,13 @@ func NewConfigRepository(db *sqlx.DB, log logger.Logger) bootstrap.ConfigReposit
|
||||
return &configRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr configRepository) Save(cfg bootstrap.Config, connections []string) (string, error) {
|
||||
func (cr configRepository) Save(cfg bootstrap.Config, chsConnIDs []string) (string, error) {
|
||||
q := `INSERT INTO configs (mainflux_thing, owner, name, client_cert, client_key, ca_cert, mainflux_key, external_id, external_key, content, state)
|
||||
VALUES (:mainflux_thing, :owner, :name, :client_cert, :client_key, :ca_cert, :mainflux_key, :external_id, :external_key, :content, :state)`
|
||||
|
||||
tx, err := cr.db.Beginx()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", errors.Wrap(errSaveDB, err)
|
||||
}
|
||||
|
||||
dbcfg := toDBConfig(cfg)
|
||||
@@ -59,27 +74,19 @@ func (cr configRepository) Save(cfg bootstrap.Config, connections []string) (str
|
||||
|
||||
cr.rollback("Failed to insert a Config", tx, err)
|
||||
|
||||
return "", e
|
||||
return "", errors.Wrap(errSaveDB, e)
|
||||
}
|
||||
|
||||
if err := insertChannels(cfg.Owner, cfg.MFChannels, tx); err != nil {
|
||||
cr.rollback("Failed to insert Channels", tx, err)
|
||||
|
||||
return "", err
|
||||
return "", errors.Wrap(errSaveChannels, err)
|
||||
}
|
||||
|
||||
if err := insertConnections(cfg, connections, tx); err != nil {
|
||||
if err := insertConnections(cfg, chsConnIDs, tx); err != nil {
|
||||
cr.rollback("Failed to insert connections", tx, err)
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
q = "DELETE FROM unknown_configs WHERE external_id = :external_id AND external_key = :external_key"
|
||||
|
||||
if _, err := tx.NamedExec(q, dbcfg); err != nil {
|
||||
cr.rollback("Failed to remove from unknown", tx, err)
|
||||
|
||||
return "", err
|
||||
return "", errors.Wrap(errSaveConnections, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
@@ -89,23 +96,23 @@ func (cr configRepository) Save(cfg bootstrap.Config, connections []string) (str
|
||||
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
|
||||
func (cr configRepository) RetrieveByID(owner, 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`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
MFThing: id,
|
||||
Owner: key,
|
||||
Owner: owner,
|
||||
}
|
||||
|
||||
if err := cr.db.QueryRowx(q, id, key).StructScan(&dbcfg); err != nil {
|
||||
if err := cr.db.QueryRowx(q, id, owner).StructScan(&dbcfg); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, bootstrap.ErrNotFound
|
||||
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return empty, err
|
||||
return empty, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
q = `SELECT mainflux_channel, name, metadata FROM channels ch
|
||||
@@ -116,7 +123,7 @@ func (cr configRepository) RetrieveByID(key, id string) (bootstrap.Config, error
|
||||
rows, err := cr.db.NamedQuery(q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, err
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -125,13 +132,13 @@ func (cr configRepository) RetrieveByID(key, id string) (bootstrap.Config, error
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
|
||||
return bootstrap.Config{}, err
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
dbch.Owner = nullString(dbcfg.Owner)
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
chans = append(chans, ch)
|
||||
}
|
||||
@@ -142,8 +149,8 @@ func (cr configRepository) RetrieveByID(key, id string) (bootstrap.Config, error
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveAll(key string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
search, params := cr.retrieveAll(key, filter)
|
||||
func (cr configRepository) RetrieveAll(owner string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
search, params := cr.retrieveAll(owner, filter)
|
||||
n := len(params)
|
||||
|
||||
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state
|
||||
@@ -161,7 +168,7 @@ func (cr configRepository) RetrieveAll(key string, filter bootstrap.Filter, offs
|
||||
configs := []bootstrap.Config{}
|
||||
|
||||
for rows.Next() {
|
||||
c := bootstrap.Config{Owner: key}
|
||||
c := bootstrap.Config{Owner: owner}
|
||||
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{}
|
||||
@@ -189,8 +196,8 @@ func (cr configRepository) RetrieveAll(key string, filter bootstrap.Filter, offs
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Config, error) {
|
||||
q := `SELECT mainflux_thing, mainflux_key, external_key, owner, name, client_cert, client_key, ca_cert, content, state
|
||||
FROM configs
|
||||
q := `SELECT mainflux_thing, mainflux_key, external_key, owner, name, client_cert, client_key, ca_cert, content, state
|
||||
FROM configs
|
||||
WHERE external_id = $1`
|
||||
dbcfg := dbConfig{
|
||||
ExternalID: externalID,
|
||||
@@ -199,9 +206,9 @@ func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Co
|
||||
if err := cr.db.QueryRowx(q, externalID).StructScan(&dbcfg); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, bootstrap.ErrNotFound
|
||||
return empty, errors.Wrap(bootstrap.ErrNotFound, err)
|
||||
}
|
||||
return empty, err
|
||||
return empty, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
q = `SELECT mainflux_channel, name, metadata FROM channels ch
|
||||
@@ -212,7 +219,7 @@ func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Co
|
||||
rows, err := cr.db.NamedQuery(q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, err
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -221,13 +228,13 @@ func (cr configRepository) RetrieveByExternalID(externalID string) (bootstrap.Co
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected thing due to %s", err))
|
||||
return bootstrap.Config{}, err
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
|
||||
return bootstrap.Config{}, err
|
||||
return bootstrap.Config{}, errors.Wrap(errRetrieve, err)
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
@@ -247,12 +254,12 @@ func (cr configRepository) Update(cfg bootstrap.Config) error {
|
||||
|
||||
res, err := cr.db.Exec(q, name, content, cfg.MFThing, cfg.Owner)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errUpdate, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errUpdate, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
@@ -282,19 +289,19 @@ func (cr configRepository) UpdateCert(owner, thingID, clientCert, clientKey, caC
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateConnections(key, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
func (cr configRepository) UpdateConnections(owner, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
tx, err := cr.db.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := insertChannels(key, channels, tx); err != nil {
|
||||
if err := insertChannels(owner, 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 err := updateConnections(owner, id, connections, tx); err != nil {
|
||||
if e, ok := err.(*pq.Error); ok {
|
||||
if e.Code.Name() == fkViolation && e.Constraint == connConstraintErr {
|
||||
return bootstrap.ErrNotFound
|
||||
@@ -312,10 +319,10 @@ func (cr configRepository) UpdateConnections(key, id string, channels []bootstra
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Remove(key, id string) error {
|
||||
func (cr configRepository) Remove(owner, 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(q, id, owner); err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
}
|
||||
|
||||
if _, err := cr.db.Exec(cleanupQuery); err != nil {
|
||||
@@ -325,10 +332,10 @@ func (cr configRepository) Remove(key, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ChangeState(key, id string, state bootstrap.State) error {
|
||||
func (cr configRepository) ChangeState(owner, 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)
|
||||
res, err := cr.db.Exec(q, state, id, owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -345,14 +352,14 @@ func (cr configRepository) ChangeState(key, id string, state bootstrap.State) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ListExisting(key string, ids []string) ([]bootstrap.Channel, error) {
|
||||
func (cr configRepository) ListExisting(owner string, ids []string) ([]bootstrap.Channel, error) {
|
||||
var channels []bootstrap.Channel
|
||||
if len(ids) == 0 {
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
q := "SELECT mainflux_channel, name, metadata FROM channels WHERE owner = $1 AND mainflux_channel = ANY ($2)"
|
||||
rows, err := cr.db.Queryx(q, key, pq.Array(ids))
|
||||
rows, err := cr.db.Queryx(q, owner, pq.Array(ids))
|
||||
if err != nil {
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
@@ -376,55 +383,6 @@ func (cr configRepository) ListExisting(key string, ids []string) ([]bootstrap.C
|
||||
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)
|
||||
@@ -432,43 +390,48 @@ func (cr configRepository) RemoveThing(id string) error {
|
||||
if _, err := cr.db.Exec(cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
|
||||
return err
|
||||
if err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateChannel(channel bootstrap.Channel) error {
|
||||
dbch, err := toDBChannel("", channel)
|
||||
func (cr configRepository) UpdateChannel(c bootstrap.Channel) error {
|
||||
dbch, err := toDBChannel("", c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q := `UPDATE channels SET name = :name, metadata = :metadata WHERE mainflux_channel = :mainflux_channel`
|
||||
_, err = cr.db.NamedExec(q, dbch)
|
||||
|
||||
return err
|
||||
if _, err = cr.db.NamedExec(q, dbch); err != nil {
|
||||
return errors.Wrap(errUpdateChannels, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveChannel(id string) error {
|
||||
q := `DELETE FROM channels WHERE mainflux_channel = $1`
|
||||
_, err := cr.db.Exec(q, id)
|
||||
|
||||
return err
|
||||
if _, err := cr.db.Exec(q, id); err != nil {
|
||||
return errors.Wrap(errRemoveChannels, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
if _, err := cr.db.Exec(q, bootstrap.Inactive, thingID, channelID); err != nil {
|
||||
return errors.Wrap(errDisconnectThing, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) retrieveAll(key string, filter bootstrap.Filter) (string, []interface{}) {
|
||||
func (cr configRepository) retrieveAll(owner string, filter bootstrap.Filter) (string, []interface{}) {
|
||||
template := `WHERE owner = $1 %s`
|
||||
params := []interface{}{key}
|
||||
params := []interface{}{owner}
|
||||
// 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.
|
||||
// Since owner is the first param, start from 2.
|
||||
counter := 2
|
||||
for k, v := range filter.FullMatch {
|
||||
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
|
||||
@@ -494,21 +457,21 @@ func (cr configRepository) rollback(content string, tx *sqlx.Tx, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func insertChannels(key string, channels []bootstrap.Channel, tx *sqlx.Tx) error {
|
||||
func insertChannels(owner string, channels []bootstrap.Channel, tx *sqlx.Tx) error {
|
||||
if len(channels) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var chans []dbChannel
|
||||
for _, ch := range channels {
|
||||
dbch, err := toDBChannel(key, ch)
|
||||
dbch, err := toDBChannel(owner, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chans = append(chans, dbch)
|
||||
}
|
||||
|
||||
q := `INSERT INTO channels (mainflux_channel, owner, name, metadata)
|
||||
q := `INSERT INTO channels (mainflux_channel, owner, name, metadata)
|
||||
VALUES (:mainflux_channel, :owner, :name, :metadata)`
|
||||
if _, err := tx.NamedExec(q, chans); err != nil {
|
||||
e := err
|
||||
@@ -526,7 +489,7 @@ func insertConnections(cfg bootstrap.Config, connections []string, tx *sqlx.Tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner)
|
||||
q := `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner)
|
||||
VALUES (:config_id, :channel_id, :config_owner, :channel_owner)`
|
||||
conns := []dbConnection{}
|
||||
for _, conn := range connections {
|
||||
@@ -543,7 +506,7 @@ func insertConnections(cfg bootstrap.Config, connections []string, tx *sqlx.Tx)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateConnections(key, id string, connections []string, tx *sqlx.Tx) error {
|
||||
func updateConnections(owner, id string, connections []string, tx *sqlx.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -552,7 +515,7 @@ func updateConnections(key, id string, connections []string, tx *sqlx.Tx) error
|
||||
WHERE config_id = $1 AND config_owner = $2 AND channel_owner = $2
|
||||
AND channel_id NOT IN ($3)`
|
||||
|
||||
res, err := tx.Exec(q, id, key, pq.Array(connections))
|
||||
res, err := tx.Exec(q, id, owner, pq.Array(connections))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -562,7 +525,7 @@ func updateConnections(key, id string, connections []string, tx *sqlx.Tx) error
|
||||
return err
|
||||
}
|
||||
|
||||
q = `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner)
|
||||
q = `INSERT INTO connections (config_id, channel_id, config_owner, channel_owner)
|
||||
VALUES (:config_id, :channel_id, :config_owner, :channel_owner)`
|
||||
|
||||
conns := []dbConnection{}
|
||||
@@ -570,8 +533,8 @@ func updateConnections(key, id string, connections []string, tx *sqlx.Tx) error
|
||||
dbconn := dbConnection{
|
||||
Config: id,
|
||||
Channel: conn,
|
||||
ConfigOwner: key,
|
||||
ChannelOwner: key,
|
||||
ConfigOwner: owner,
|
||||
ChannelOwner: owner,
|
||||
}
|
||||
conns = append(conns, dbconn)
|
||||
}
|
||||
@@ -678,7 +641,7 @@ func toDBChannel(owner string, ch bootstrap.Channel) (dbChannel, error) {
|
||||
|
||||
metadata, err := json.Marshal(ch.Metadata)
|
||||
if err != nil {
|
||||
return dbChannel{}, err
|
||||
return dbChannel{}, errors.Wrap(errMarshalChannel, err)
|
||||
}
|
||||
|
||||
dbch.Metadata = string(metadata)
|
||||
@@ -695,7 +658,7 @@ func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
|
||||
return bootstrap.Channel{}, err
|
||||
return bootstrap.Channel{}, errors.Wrap(errUnmarshalChannel, err)
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -90,7 +91,7 @@ func TestSave(t *testing.T) {
|
||||
}
|
||||
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))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +147,7 @@ func TestRetrieveByID(t *testing.T) {
|
||||
}
|
||||
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))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +266,7 @@ func TestRetrieveByExternalID(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByExternalID(tc.externalID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +311,7 @@ func TestUpdate(t *testing.T) {
|
||||
}
|
||||
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))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,7 +339,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
thingID string
|
||||
owner string
|
||||
cert string
|
||||
certKey string
|
||||
@@ -347,7 +348,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong owner",
|
||||
key: "",
|
||||
thingID: "",
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "",
|
||||
@@ -356,7 +357,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
key: c.MFKey,
|
||||
thingID: c.MFThing,
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "ca",
|
||||
@@ -365,8 +366,8 @@ func TestUpdateCert(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.UpdateCert(tc.owner, tc.key, tc.cert, tc.key, tc.ca)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
err := repo.UpdateCert(tc.owner, tc.thingID, tc.cert, tc.certKey, tc.ca)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +399,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
owner string
|
||||
id string
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
@@ -406,7 +407,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
desc: "update connections of non-existing config",
|
||||
key: config.Owner,
|
||||
owner: config.Owner,
|
||||
id: "unknown",
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
@@ -414,7 +415,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "update connections",
|
||||
key: config.Owner,
|
||||
owner: config.Owner,
|
||||
id: c.MFThing,
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
@@ -422,7 +423,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "update connections with existing channels",
|
||||
key: config.Owner,
|
||||
owner: config.Owner,
|
||||
id: c2,
|
||||
channels: nil,
|
||||
connections: channels,
|
||||
@@ -430,7 +431,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "update connections no channels",
|
||||
key: config.Owner,
|
||||
owner: config.Owner,
|
||||
id: c.MFThing,
|
||||
channels: nil,
|
||||
connections: nil,
|
||||
@@ -438,8 +439,8 @@ func TestUpdateConnections(t *testing.T) {
|
||||
},
|
||||
}
|
||||
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))
|
||||
err := repo.UpdateConnections(tc.owner, tc.id, tc.channels, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,7 +467,7 @@ func TestRemove(t *testing.T) {
|
||||
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))
|
||||
require.True(t, errors.Contains(err, bootstrap.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, bootstrap.ErrNotFound, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,7 +523,7 @@ func TestChangeState(t *testing.T) {
|
||||
}
|
||||
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))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,99 +550,36 @@ func TestListExisting(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
owner string
|
||||
connections []string
|
||||
existing []bootstrap.Channel
|
||||
}{
|
||||
{
|
||||
desc: "list all existing channels",
|
||||
key: c.Owner,
|
||||
owner: c.Owner,
|
||||
connections: channels,
|
||||
existing: chs,
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels",
|
||||
key: c.Owner,
|
||||
owner: c.Owner,
|
||||
connections: []string{channels[0], "5"},
|
||||
existing: []bootstrap.Channel{chs[0]},
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels empty",
|
||||
key: c.Owner,
|
||||
owner: c.Owner,
|
||||
connections: []string{"5", "6"},
|
||||
existing: []bootstrap.Channel{},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
existing, err := repo.ListExisting(tc.key, tc.connections)
|
||||
existing, err := repo.ListExisting(tc.owner, 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: "unknown",
|
||||
externalKey: "unknown",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save invalid unknown",
|
||||
externalID: "unknown",
|
||||
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, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
repo.SaveUnknown(id.String(), id.String())
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -91,6 +91,15 @@ func migrateDB(db *sqlx.DB) error {
|
||||
"DROP TABLE unknown_configs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_2",
|
||||
Up: []string{
|
||||
"DROP TABLE IF EXISTS unknown_configs",
|
||||
},
|
||||
Down: []string{
|
||||
"CREATE TABLE IF NOT EXISTS unknown_configs",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
dockertest "gopkg.in/ory-am/dockertest.v3"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -69,10 +69,11 @@ func TestMain(m *testing.M) {
|
||||
if db, err = postgres.Connect(dbConfig); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
dockertest "gopkg.in/ory-am/dockertest.v3"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -31,8 +31,8 @@ func NewEventStoreMiddleware(svc bootstrap.Service, client *redis.Client) bootst
|
||||
}
|
||||
}
|
||||
|
||||
func (es eventStore) Add(key string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
saved, err := es.svc.Add(key, cfg)
|
||||
func (es eventStore) Add(token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
saved, err := es.svc.Add(token, cfg)
|
||||
if err != nil {
|
||||
return saved, err
|
||||
}
|
||||
@@ -57,12 +57,12 @@ func (es eventStore) Add(key string, cfg bootstrap.Config) (bootstrap.Config, er
|
||||
return saved, err
|
||||
}
|
||||
|
||||
func (es eventStore) View(key, id string) (bootstrap.Config, error) {
|
||||
return es.svc.View(key, id)
|
||||
func (es eventStore) View(token, id string) (bootstrap.Config, error) {
|
||||
return es.svc.View(token, id)
|
||||
}
|
||||
|
||||
func (es eventStore) Update(key string, cfg bootstrap.Config) error {
|
||||
if err := es.svc.Update(key, cfg); err != nil {
|
||||
func (es eventStore) Update(token string, cfg bootstrap.Config) error {
|
||||
if err := es.svc.Update(token, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -78,12 +78,12 @@ func (es eventStore) Update(key string, cfg bootstrap.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateCert(key, thingKey, clientCert, clientKey, caCert string) error {
|
||||
return es.svc.UpdateCert(key, thingKey, clientCert, clientKey, caCert)
|
||||
func (es eventStore) UpdateCert(token, thingKey, clientCert, clientKey, caCert string) error {
|
||||
return es.svc.UpdateCert(token, thingKey, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateConnections(key, id string, connections []string) error {
|
||||
if err := es.svc.UpdateConnections(key, id, connections); err != nil {
|
||||
func (es eventStore) UpdateConnections(token, id string, connections []string) error {
|
||||
if err := es.svc.UpdateConnections(token, id, connections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -98,12 +98,12 @@ func (es eventStore) UpdateConnections(key, id string, connections []string) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es eventStore) List(key string, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
return es.svc.List(key, filter, offset, limit)
|
||||
func (es eventStore) List(token string, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
return es.svc.List(token, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (es eventStore) Remove(key, id string) error {
|
||||
if err := es.svc.Remove(key, id); err != nil {
|
||||
func (es eventStore) Remove(token, id string) error {
|
||||
if err := es.svc.Remove(token, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -135,8 +135,8 @@ func (es eventStore) Bootstrap(externalKey, externalID string, secure bool) (boo
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es eventStore) ChangeState(key, id string, state bootstrap.State) error {
|
||||
if err := es.svc.ChangeState(key, id, state); err != nil {
|
||||
func (es eventStore) ChangeState(token, id string, state bootstrap.State) error {
|
||||
if err := es.svc.ChangeState(token, id, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,13 @@ import (
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/opentracing/opentracing-go/mocktracer"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
"github.com/mainflux/mainflux/bootstrap/mocks"
|
||||
"github.com/mainflux/mainflux/bootstrap/redis/producer"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -29,8 +30,6 @@ const (
|
||||
streamID = "mainflux.bootstrap"
|
||||
email = "user@example.com"
|
||||
validToken = "validToken"
|
||||
unknownID = "1"
|
||||
unknownKey = "2"
|
||||
channelsNum = 3
|
||||
defaultTimout = 5
|
||||
|
||||
@@ -63,7 +62,7 @@ var (
|
||||
)
|
||||
|
||||
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
configs := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
|
||||
configs := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
@@ -109,14 +108,14 @@ func TestAdd(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "create config successfully",
|
||||
config: config,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": "1",
|
||||
@@ -132,7 +131,7 @@ func TestAdd(t *testing.T) {
|
||||
{
|
||||
desc: "create invalid config",
|
||||
config: invalidConfig,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
event: nil,
|
||||
},
|
||||
@@ -140,8 +139,8 @@ func TestAdd(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
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))
|
||||
_, err := svc.Add(tc.token, tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
@@ -204,14 +203,14 @@ func TestUpdate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "update config successfully",
|
||||
config: modified,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": modified.MFThing,
|
||||
@@ -224,7 +223,7 @@ func TestUpdate(t *testing.T) {
|
||||
{
|
||||
desc: "update non-existing config",
|
||||
config: nonExisting,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
event: nil,
|
||||
},
|
||||
@@ -232,7 +231,7 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.Update(tc.key, tc.config)
|
||||
err := svc.Update(tc.token, tc.config)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
@@ -267,7 +266,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
key string
|
||||
token string
|
||||
connections []string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
@@ -275,7 +274,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
{
|
||||
desc: "update connections successfully",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
connections: []string{"2"},
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
@@ -288,7 +287,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
{
|
||||
desc: "update connections unsuccessfully",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
connections: []string{"256"},
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
event: nil,
|
||||
@@ -297,8 +296,8 @@ func TestUpdateConnections(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
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))
|
||||
err := svc.UpdateConnections(tc.token, tc.id, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
@@ -352,15 +351,15 @@ func TestRemove(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
}{
|
||||
{
|
||||
desc: "remove config successfully",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
desc: "remove config successfully",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
"thing_id": saved.MFThing,
|
||||
"timestamp": time.Now().Unix(),
|
||||
@@ -370,7 +369,7 @@ func TestRemove(t *testing.T) {
|
||||
{
|
||||
desc: "remove config with invalid credentials",
|
||||
id: saved.MFThing,
|
||||
key: "",
|
||||
token: "",
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
event: nil,
|
||||
},
|
||||
@@ -378,7 +377,7 @@ func TestRemove(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.Remove(tc.key, tc.id)
|
||||
err := svc.Remove(tc.token, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
@@ -448,7 +447,7 @@ func TestBootstrap(t *testing.T) {
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
_, err := svc.Bootstrap(tc.externalKey, tc.externalID, false)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
Streams: []string{streamID, lastID},
|
||||
@@ -484,7 +483,7 @@ func TestChangeState(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
key string
|
||||
token string
|
||||
state bootstrap.State
|
||||
err error
|
||||
event map[string]interface{}
|
||||
@@ -492,7 +491,7 @@ func TestChangeState(t *testing.T) {
|
||||
{
|
||||
desc: "change state to active",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
state: bootstrap.Active,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
@@ -505,7 +504,7 @@ func TestChangeState(t *testing.T) {
|
||||
{
|
||||
desc: "change state invalid credentials",
|
||||
id: saved.MFThing,
|
||||
key: "",
|
||||
token: "",
|
||||
state: bootstrap.Inactive,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
event: nil,
|
||||
@@ -514,7 +513,7 @@ func TestChangeState(t *testing.T) {
|
||||
|
||||
lastID := "0"
|
||||
for _, tc := range cases {
|
||||
err := svc.ChangeState(tc.key, tc.id, tc.state)
|
||||
err := svc.ChangeState(tc.token, tc.id, tc.state)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
streams := redisClient.XRead(&redis.XReadArgs{
|
||||
|
||||
+136
-91
@@ -8,11 +8,11 @@ import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -31,7 +31,30 @@ var (
|
||||
|
||||
// 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")
|
||||
ErrThings = errors.New("failed to receive response from Things service")
|
||||
|
||||
// ErrExternalKeyNotFound indicates a non-existent bootstrap configuration for given external key
|
||||
ErrExternalKeyNotFound = errors.New("failed to get bootstrap configuration for given external key")
|
||||
|
||||
// ErrSecureBootstrap indicates error in getting bootstrap configuration for given encrypted external key
|
||||
ErrSecureBootstrap = errors.New("failed to get bootstrap configuration for given encrypted external key")
|
||||
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
ErrBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
|
||||
errAddBootstrap = errors.New("failed to add bootstrap configuration")
|
||||
errUpdateConnections = errors.New("failed to update connections")
|
||||
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
|
||||
errChangeState = errors.New("failed to change state of bootstrap configuration")
|
||||
errUpdateChannel = errors.New("failed to update channel")
|
||||
errRemoveConfig = errors.New("failed to remove bootstrap configuration")
|
||||
errRemoveChannel = errors.New("failed to remove channel")
|
||||
errCreateThing = errors.New("failed to create thing")
|
||||
errDisconnectThing = errors.New("failed to disconnect thing")
|
||||
errThingNotFound = errors.New("thing not found")
|
||||
errCheckChannels = errors.New("failed to check if channels exists")
|
||||
errConnectionChannels = errors.New("failed to check channels connections")
|
||||
errUpdateCert = errors.New("failed to update cert")
|
||||
)
|
||||
|
||||
var _ Service = (*bootstrapService)(nil)
|
||||
@@ -39,49 +62,49 @@ 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)
|
||||
// Add adds new Thing Config to the user identified by the provided token.
|
||||
Add(token string, cfg Config) (Config, error)
|
||||
|
||||
// View returns Thing Config with given ID belonging to the user identified by the given key.
|
||||
View(string, string) (Config, error)
|
||||
// View returns Thing Config with given ID belonging to the user identified by the given token.
|
||||
View(token, id string) (Config, error)
|
||||
|
||||
// Update updates editable fields of the provided Config.
|
||||
Update(string, Config) error
|
||||
Update(token string, cfg Config) error
|
||||
|
||||
// UpdateCert updates an existing Config certificate and key.
|
||||
// UpdateCert updates an existing Config certificate and token.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(string, string, string, string, string) error
|
||||
UpdateCert(token, thingID, clientCert, clientKey, caCert string) error
|
||||
|
||||
// UpdateConnections updates list of Channels related to given Config.
|
||||
UpdateConnections(string, string, []string) error
|
||||
UpdateConnections(token, id string, connections []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)
|
||||
// user identified by the given token.
|
||||
List(token string, filter Filter, offset, limit uint64) (ConfigsPage, error)
|
||||
|
||||
// Remove removes Config with specified key that belongs to the user identified by the given key.
|
||||
Remove(string, string) error
|
||||
// Remove removes Config with specified token that belongs to the user identified by the given token.
|
||||
Remove(token, id string) error
|
||||
|
||||
// Bootstrap returns Config to the Thing with provided external ID using external key.
|
||||
Bootstrap(string, string, bool) (Config, error)
|
||||
Bootstrap(externalKey, externalID string, secure bool) (Config, error)
|
||||
|
||||
// ChangeState changes state of the Thing with given ID and owner.
|
||||
ChangeState(string, string, State) error
|
||||
ChangeState(token, id string, state 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
|
||||
UpdateChannelHandler(channel Channel) error
|
||||
|
||||
// RemoveConfigHandler removes Configuration with id received from an event.
|
||||
RemoveConfigHandler(id string) error
|
||||
|
||||
// RemoveChannelHandler removes Channel with id received from an event.
|
||||
RemoveChannelHandler(string) error
|
||||
RemoveChannelHandler(id string) error
|
||||
|
||||
// DisconnectHandler changes state of the Config when connect/disconnect event occurs.
|
||||
DisconnectThingHandler(string, string) error
|
||||
DisconnectThingHandler(channelID, thingID string) error
|
||||
}
|
||||
|
||||
// ConfigReader is used to parse Config into format which will be encoded
|
||||
@@ -110,8 +133,8 @@ func New(auth mainflux.AuthNServiceClient, configs ConfigRepository, sdk mfsdk.S
|
||||
}
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Add(key string, cfg Config) (Config, error) {
|
||||
owner, err := bs.identify(key)
|
||||
func (bs bootstrapService) Add(token string, cfg Config) (Config, error) {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
@@ -121,33 +144,34 @@ func (bs bootstrapService) Add(key string, cfg Config) (Config, error) {
|
||||
// Check if channels exist. This is the way to prevent fetching channels that already exist.
|
||||
existing, err := bs.configs.ListExisting(owner, toConnect)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
return Config{}, errors.Wrap(errCheckChannels, err)
|
||||
}
|
||||
|
||||
cfg.MFChannels, err = bs.connectionChannels(toConnect, bs.toIDList(existing), key)
|
||||
cfg.MFChannels, err = bs.connectionChannels(toConnect, bs.toIDList(existing), token)
|
||||
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
return Config{}, errors.Wrap(errConnectionChannels, err)
|
||||
}
|
||||
|
||||
id := cfg.MFThing
|
||||
mfThing, err := bs.thing(key, id)
|
||||
mfThing, err := bs.thing(token, id)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
return Config{}, errors.Wrap(errAddBootstrap, err)
|
||||
}
|
||||
|
||||
cfg.MFThing = mfThing.ID
|
||||
cfg.Owner = owner
|
||||
cfg.State = Inactive
|
||||
cfg.MFKey = mfThing.Key
|
||||
saved, err := bs.configs.Save(cfg, toConnect)
|
||||
|
||||
saved, err := bs.configs.Save(cfg, toConnect)
|
||||
if err != nil {
|
||||
if id == "" {
|
||||
// Fail silently.
|
||||
bs.sdk.DeleteThing(cfg.MFThing, key)
|
||||
if errT := bs.sdk.DeleteThing(cfg.MFThing, token); errT != nil {
|
||||
err = errors.Wrap(err, errT)
|
||||
}
|
||||
}
|
||||
return Config{}, err
|
||||
return Config{}, errors.Wrap(errAddBootstrap, err)
|
||||
}
|
||||
|
||||
cfg.MFThing = saved
|
||||
@@ -156,8 +180,8 @@ func (bs bootstrapService) Add(key string, cfg Config) (Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) View(key, id string) (Config, error) {
|
||||
owner, err := bs.identify(key)
|
||||
func (bs bootstrapService) View(token, id string) (Config, error) {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
@@ -165,8 +189,8 @@ func (bs bootstrapService) View(key, id string) (Config, error) {
|
||||
return bs.configs.RetrieveByID(owner, id)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Update(key string, cfg Config) error {
|
||||
owner, err := bs.identify(key)
|
||||
func (bs bootstrapService) Update(token string, cfg Config) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -176,23 +200,26 @@ func (bs bootstrapService) Update(key string, cfg Config) error {
|
||||
return bs.configs.Update(cfg)
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateCert(key, thingID, clientCert, clientKey, caCert string) error {
|
||||
owner, err := bs.identify(key)
|
||||
func (bs bootstrapService) UpdateCert(token, thingID, clientCert, clientKey, caCert string) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bs.configs.UpdateCert(owner, thingID, clientCert, clientKey, caCert)
|
||||
if err := bs.configs.UpdateCert(owner, thingID, clientCert, clientKey, caCert); err != nil {
|
||||
return errors.Wrap(errUpdateCert, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateConnections(key, id string, connections []string) error {
|
||||
owner, err := bs.identify(key)
|
||||
func (bs bootstrapService) UpdateConnections(token, id string, connections []string) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := bs.configs.RetrieveByID(owner, id)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
add, remove := bs.updateList(cfg, connections)
|
||||
@@ -200,12 +227,12 @@ func (bs bootstrapService) UpdateConnections(key, id string, connections []strin
|
||||
// Check if channels exist. This is the way to prevent fetching channels that already exist.
|
||||
existing, err := bs.configs.ListExisting(owner, connections)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
channels, err := bs.connectionChannels(connections, bs.toIDList(existing), key)
|
||||
channels, err := bs.connectionChannels(connections, bs.toIDList(existing), token)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
cfg.MFChannels = channels
|
||||
@@ -217,8 +244,8 @@ func (bs bootstrapService) UpdateConnections(key, id string, connections []strin
|
||||
}
|
||||
|
||||
for _, c := range disconnect {
|
||||
if err := bs.sdk.DisconnectThing(id, c, key); err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
if err := bs.sdk.DisconnectThing(id, c, token); err != nil {
|
||||
if errors.Contains(err, mfsdk.ErrFailedDisconnect) {
|
||||
continue
|
||||
}
|
||||
return ErrThings
|
||||
@@ -226,8 +253,12 @@ func (bs bootstrapService) UpdateConnections(key, id string, connections []strin
|
||||
}
|
||||
|
||||
for _, c := range connect {
|
||||
if err := bs.sdk.ConnectThing(id, c, key); err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
conIDs := mfsdk.ConnectionIDs{
|
||||
ChannelIDs: []string{c},
|
||||
ThingIDs: []string{id},
|
||||
}
|
||||
if err := bs.sdk.Connect(conIDs, token); err != nil {
|
||||
if errors.Contains(err, mfsdk.ErrFailedConnect) {
|
||||
return ErrMalformedEntity
|
||||
}
|
||||
return ErrThings
|
||||
@@ -237,62 +268,56 @@ func (bs bootstrapService) UpdateConnections(key, id string, connections []strin
|
||||
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)
|
||||
func (bs bootstrapService) List(token string, filter Filter, offset, limit uint64) (ConfigsPage, error) {
|
||||
owner, err := bs.identify(token)
|
||||
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)
|
||||
func (bs bootstrapService) Remove(token, id string) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bs.configs.Remove(owner, id)
|
||||
if err := bs.configs.Remove(owner, id); err != nil {
|
||||
return errors.Wrap(errRemoveBootstrap, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Bootstrap(externalKey, externalID string, secure bool) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByExternalID(externalID)
|
||||
if err != nil {
|
||||
if err == ErrNotFound {
|
||||
bs.configs.SaveUnknown(externalKey, externalID)
|
||||
return Config{}, ErrNotFound
|
||||
}
|
||||
return cfg, err
|
||||
return cfg, errors.Wrap(ErrBootstrap, err)
|
||||
}
|
||||
|
||||
if secure {
|
||||
dec, err := bs.dec(externalKey)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
return Config{}, errors.Wrap(ErrSecureBootstrap, err)
|
||||
}
|
||||
externalKey = dec
|
||||
}
|
||||
|
||||
if cfg.ExternalKey != externalKey {
|
||||
return Config{}, ErrNotFound
|
||||
return Config{}, errors.Wrap(ErrExternalKeyNotFound, ErrNotFound)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ChangeState(key, id string, state State) error {
|
||||
owner, err := bs.identify(key)
|
||||
func (bs bootstrapService) ChangeState(token, id string, state State) error {
|
||||
owner, err := bs.identify(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := bs.configs.RetrieveByID(owner, id)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
|
||||
if cfg.State == state {
|
||||
@@ -302,38 +327,56 @@ func (bs bootstrapService) ChangeState(key, id string, state State) error {
|
||||
switch state {
|
||||
case Active:
|
||||
for _, c := range cfg.MFChannels {
|
||||
if err := bs.sdk.ConnectThing(cfg.MFThing, c.ID, key); err != nil {
|
||||
conIDs := mfsdk.ConnectionIDs{
|
||||
ChannelIDs: []string{c.ID},
|
||||
ThingIDs: []string{cfg.MFThing},
|
||||
}
|
||||
if err := bs.sdk.Connect(conIDs, token); err != nil {
|
||||
return ErrThings
|
||||
}
|
||||
}
|
||||
case Inactive:
|
||||
for _, c := range cfg.MFChannels {
|
||||
if err := bs.sdk.DisconnectThing(cfg.MFThing, c.ID, key); err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
if err := bs.sdk.DisconnectThing(cfg.MFThing, c.ID, token); err != nil {
|
||||
if errors.Contains(err, mfsdk.ErrFailedDisconnect) {
|
||||
continue
|
||||
}
|
||||
return ErrThings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bs.configs.ChangeState(owner, id, state)
|
||||
if err := bs.configs.ChangeState(owner, id, state); err != nil {
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateChannelHandler(channel Channel) error {
|
||||
return bs.configs.UpdateChannel(channel)
|
||||
if err := bs.configs.UpdateChannel(channel); err != nil {
|
||||
return errors.Wrap(errUpdateChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveConfigHandler(id string) error {
|
||||
return bs.configs.RemoveThing(id)
|
||||
if err := bs.configs.RemoveThing(id); err != nil {
|
||||
return errors.Wrap(errRemoveConfig, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveChannelHandler(id string) error {
|
||||
return bs.configs.RemoveChannel(id)
|
||||
if err := bs.configs.RemoveChannel(id); err != nil {
|
||||
return errors.Wrap(errRemoveChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) DisconnectThingHandler(channelID, thingID string) error {
|
||||
return bs.configs.DisconnectThing(channelID, thingID)
|
||||
if err := bs.configs.DisconnectThing(channelID, thingID); err != nil {
|
||||
return errors.Wrap(errDisconnectThing, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) identify(token string) (string, error) {
|
||||
@@ -349,34 +392,36 @@ func (bs bootstrapService) identify(token string) (string, error) {
|
||||
}
|
||||
|
||||
// Method thing retrieves Mainflux Thing creating one if an empty ID is passed.
|
||||
func (bs bootstrapService) thing(key, id string) (mfsdk.Thing, error) {
|
||||
func (bs bootstrapService) thing(token, id string) (mfsdk.Thing, error) {
|
||||
thingID := id
|
||||
var err error
|
||||
|
||||
if id == "" {
|
||||
thingID, err = bs.sdk.CreateThing(mfsdk.Thing{}, key)
|
||||
thingID, err = bs.sdk.CreateThing(mfsdk.Thing{}, token)
|
||||
if err != nil {
|
||||
return mfsdk.Thing{}, err
|
||||
return mfsdk.Thing{}, errors.Wrap(errCreateThing, err)
|
||||
}
|
||||
}
|
||||
|
||||
thing, err := bs.sdk.Thing(thingID, key)
|
||||
thing, err := bs.sdk.Thing(thingID, token)
|
||||
if err != nil {
|
||||
if err == mfsdk.ErrNotFound {
|
||||
return mfsdk.Thing{}, ErrNotFound
|
||||
if errors.Contains(err, mfsdk.ErrFailedFetch) {
|
||||
return mfsdk.Thing{}, errors.Wrap(errThingNotFound, ErrNotFound)
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
bs.sdk.DeleteThing(thingID, key)
|
||||
if errT := bs.sdk.DeleteThing(thingID, token); errT != nil {
|
||||
err = errors.Wrap(err, errT)
|
||||
}
|
||||
}
|
||||
|
||||
return mfsdk.Thing{}, ErrThings
|
||||
return mfsdk.Thing{}, errors.Wrap(ErrThings, err)
|
||||
}
|
||||
|
||||
return thing, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) connectionChannels(channels, existing []string, key string) ([]Channel, error) {
|
||||
func (bs bootstrapService) connectionChannels(channels, existing []string, token string) ([]Channel, error) {
|
||||
add := make(map[string]bool, len(channels))
|
||||
for _, ch := range channels {
|
||||
add[ch] = true
|
||||
@@ -390,9 +435,9 @@ func (bs bootstrapService) connectionChannels(channels, existing []string, key s
|
||||
|
||||
var ret []Channel
|
||||
for id := range add {
|
||||
ch, err := bs.sdk.Channel(id, key)
|
||||
ch, err := bs.sdk.Channel(id, token)
|
||||
if err != nil {
|
||||
return nil, ErrMalformedEntity
|
||||
return nil, errors.Wrap(ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
ret = append(ret, Channel{
|
||||
|
||||
+92
-112
@@ -20,7 +20,8 @@ import (
|
||||
"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/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
httpapi "github.com/mainflux/mainflux/things/api/things/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -32,8 +33,6 @@ const (
|
||||
invalidToken = "invalidToken"
|
||||
email = "test@example.com"
|
||||
unknown = "unknown"
|
||||
unknownID = "1"
|
||||
unknownKey = "2"
|
||||
channelsNum = 3
|
||||
)
|
||||
|
||||
@@ -55,7 +54,7 @@ var (
|
||||
)
|
||||
|
||||
func newService(auth mainflux.AuthNServiceClient, url string) bootstrap.Service {
|
||||
things := mocks.NewConfigsRepository(map[string]string{unknownID: unknownKey})
|
||||
things := mocks.NewConfigsRepository()
|
||||
config := mfsdk.Config{
|
||||
BaseURL: url,
|
||||
}
|
||||
@@ -115,38 +114,38 @@ func TestAdd(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "add a new config",
|
||||
config: config,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "add a config with an invalid ID",
|
||||
config: neID,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "add a config with wrong credentials",
|
||||
config: config,
|
||||
key: invalidToken,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "add a config with invalid list of channels",
|
||||
config: wrongChannels,
|
||||
key: validToken,
|
||||
token: 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))
|
||||
_, err := svc.Add(tc.token, tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,34 +159,34 @@ func TestView(t *testing.T) {
|
||||
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 string
|
||||
id string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "view an existing config",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
desc: "view an existing config",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "view a non-existing config",
|
||||
id: unknown,
|
||||
key: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
desc: "view a non-existing config",
|
||||
id: unknown,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "view a config with wrong credentials",
|
||||
id: config.MFThing,
|
||||
key: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
desc: "view a config with wrong credentials",
|
||||
id: config.MFThing,
|
||||
token: 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))
|
||||
_, err := svc.View(tc.token, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,32 +213,32 @@ func TestUpdate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update a config with state Created",
|
||||
config: modifiedCreated,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update a non-existing config",
|
||||
config: nonExisting,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config with wrong credentials",
|
||||
config: saved,
|
||||
key: invalidToken,
|
||||
token: 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))
|
||||
err := svc.Update(tc.token, tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +257,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
token string
|
||||
thingKey string
|
||||
clientCert string
|
||||
clientKey string
|
||||
@@ -271,7 +270,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
clientCert: "newCert",
|
||||
clientKey: "newKey",
|
||||
caCert: "newCert",
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
@@ -281,8 +280,8 @@ func TestUpdateCert(t *testing.T) {
|
||||
clientKey: "newKey",
|
||||
caCert: "newCert",
|
||||
|
||||
key: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update config cert with wrong credentials",
|
||||
@@ -290,14 +289,14 @@ func TestUpdateCert(t *testing.T) {
|
||||
clientCert: "newCert",
|
||||
clientKey: "newKey",
|
||||
caCert: "newCert",
|
||||
key: invalidToken,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := svc.UpdateCert(tc.key, tc.thingKey, tc.clientCert, tc.clientKey, tc.caCert)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
err := svc.UpdateCert(tc.token, tc.thingKey, tc.clientCert, tc.clientKey, tc.caCert)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,42 +326,42 @@ func TestUpdateConnections(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
key string
|
||||
token string
|
||||
id string
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update connections for config with state Inactive",
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
id: created.MFThing,
|
||||
connections: []string{"2"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections for config with state Active",
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
id: active.MFThing,
|
||||
connections: []string{"3"},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections for non-existing config",
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
id: "",
|
||||
connections: []string{"3"},
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update connections with invalid channels",
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
id: created.MFThing,
|
||||
connections: []string{"wrong"},
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "update connections a config with wrong credentials",
|
||||
key: invalidToken,
|
||||
token: invalidToken,
|
||||
id: created.MFKey,
|
||||
connections: []string{"2", "3"},
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
@@ -370,8 +369,8 @@ func TestUpdateConnections(t *testing.T) {
|
||||
}
|
||||
|
||||
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))
|
||||
err := svc.UpdateConnections(tc.token, tc.id, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,18 +398,13 @@ func TestList(t *testing.T) {
|
||||
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
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
@@ -422,7 +416,7 @@ func TestList(t *testing.T) {
|
||||
Configs: saved[0:10],
|
||||
},
|
||||
filter: bootstrap.Filter{},
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: nil,
|
||||
@@ -436,7 +430,7 @@ func TestList(t *testing.T) {
|
||||
Configs: saved[95:96],
|
||||
},
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "95"}},
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
err: nil,
|
||||
@@ -445,7 +439,7 @@ func TestList(t *testing.T) {
|
||||
desc: "list configs unauthorized",
|
||||
config: bootstrap.ConfigsPage{},
|
||||
filter: bootstrap.Filter{},
|
||||
key: invalidToken,
|
||||
token: invalidToken,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
@@ -459,7 +453,7 @@ func TestList(t *testing.T) {
|
||||
Configs: saved[95:],
|
||||
},
|
||||
filter: bootstrap.Filter{},
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
offset: 95,
|
||||
limit: 10,
|
||||
err: nil,
|
||||
@@ -473,32 +467,18 @@ func TestList(t *testing.T) {
|
||||
Configs: []bootstrap.Config{saved[41]},
|
||||
},
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
|
||||
key: validToken,
|
||||
token: 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)
|
||||
result, err := svc.List(tc.token, 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))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,40 +492,40 @@ func TestRemove(t *testing.T) {
|
||||
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 string
|
||||
id string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "view a config with wrong credentials",
|
||||
id: saved.MFThing,
|
||||
key: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
desc: "view a config with wrong credentials",
|
||||
id: saved.MFThing,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "remove an existing config",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
desc: "remove an existing config",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove removed config",
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
desc: "remove removed config",
|
||||
id: saved.MFThing,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove non-existing config",
|
||||
id: unknown,
|
||||
key: validToken,
|
||||
err: nil,
|
||||
desc: "remove non-existing config",
|
||||
id: unknown,
|
||||
token: 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))
|
||||
err := svc.Remove(tc.token, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,7 +586,7 @@ func TestBootstrap(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
config, err := svc.Bootstrap(tc.externalKey, tc.externalID, tc.encrypted)
|
||||
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))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,49 +603,49 @@ func TestChangeState(t *testing.T) {
|
||||
desc string
|
||||
state bootstrap.State
|
||||
id string
|
||||
key string
|
||||
token string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "change state with wrong credentials",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
key: invalidToken,
|
||||
token: invalidToken,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "change state of non-existing config",
|
||||
state: bootstrap.Active,
|
||||
id: unknown,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: bootstrap.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state to Active",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to current state",
|
||||
state: bootstrap.Active,
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: validToken,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to Inactive",
|
||||
state: bootstrap.Inactive,
|
||||
id: saved.MFThing,
|
||||
key: validToken,
|
||||
token: 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))
|
||||
err := svc.ChangeState(tc.token, tc.id, tc.state)
|
||||
assert.True(t, errors.Contains(err, tc.err), err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,7 +682,7 @@ func TestUpdateChannelHandler(t *testing.T) {
|
||||
|
||||
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))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,7 +714,7 @@ func TestRemoveChannelHandler(t *testing.T) {
|
||||
|
||||
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))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,7 +746,7 @@ func TestRemoveCoinfigHandler(t *testing.T) {
|
||||
|
||||
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))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -801,6 +781,6 @@ func TestDisconnectThingsHandler(t *testing.T) {
|
||||
|
||||
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))
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,520 +0,0 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: Mainflux Bootstrap service
|
||||
description: HTTP API for managing platform things 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 configuration'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/ConfigAuth"
|
||||
- $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/bootstrap/secure/{externalId}:
|
||||
get:
|
||||
summary: Retrieves configuration
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and encrypted external key.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/EncConfigAuth"
|
||||
- $ref: "#/parameters/ExternalId"
|
||||
responses:
|
||||
200:
|
||||
description: |
|
||||
Data retrieved. In this case, Bootstrap response is encrypted using
|
||||
the secret key, so an actual response is in the binary format.
|
||||
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 successful removal the service will ensure
|
||||
that the removed config is disconnected from all of the Mainflux channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
responses:
|
||||
204:
|
||||
description: Config removed.
|
||||
400:
|
||||
description: Failed due to malformed config ID.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/things/configs/certs/{configId}:
|
||||
patch:
|
||||
summary: Updates certs
|
||||
description: |
|
||||
Update is performed by replacing the current certificate data with values
|
||||
provided in a request payload.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ConfigId"
|
||||
- name: config
|
||||
description: JSON-formatted document describing the updated thing.
|
||||
in: body
|
||||
schema:
|
||||
$ref: "#/definitions/ConfigUpdateCertReq"
|
||||
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/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:
|
||||
$ref: "#/definitions/ConfigUpdateConnReq"
|
||||
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 unsuccessfully 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
|
||||
ConfigAuth:
|
||||
name: configAuthorization
|
||||
description: Configuration external key.
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
EncConfigAuth:
|
||||
name: configAuthorization
|
||||
description: |
|
||||
Hex-encoded configuration external key encrypted using
|
||||
the AES algorithm and SHA256 sum of the external key
|
||||
itself as an encryption key.
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
ConfigId:
|
||||
name: configId
|
||||
description: Unique Config identifier. It's the ID of the corresponding Thing.
|
||||
in: path
|
||||
type: string
|
||||
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 occurred.
|
||||
|
||||
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:
|
||||
- configs
|
||||
State:
|
||||
type: integer
|
||||
enum:
|
||||
- active
|
||||
- inactive
|
||||
ConfigRes:
|
||||
type: object
|
||||
properties:
|
||||
mainflux_id:
|
||||
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 unique 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_id:
|
||||
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.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certrificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
ca_cert:
|
||||
type: string
|
||||
description: Issuing CA certificate.
|
||||
required:
|
||||
- mainflux_id
|
||||
- mainflux_key
|
||||
- mainflux_channels
|
||||
- content
|
||||
|
||||
ConfigReq:
|
||||
type: object
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique 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:
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- name
|
||||
ConfigUpdateConnReq:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
ConfigUpdateCertReq:
|
||||
type: object
|
||||
properties:
|
||||
client_cert:
|
||||
type: string
|
||||
client_key:
|
||||
type: string
|
||||
ca_cert:
|
||||
type: string
|
||||
@@ -0,0 +1,53 @@
|
||||
# Certs Service
|
||||
Issues certificates for things. `Certs` service can create certificates to be used when `Mainflux` is deployed to support mTLS.
|
||||
Certificate service can create certificates in two modes:
|
||||
1. Development mode - to be used when no PKI is deployed, this works similar to the [make thing_cert](../docker/ssl/Makefile)
|
||||
2. PKI mode - certificates issued by PKI, when you deploy `Vault` as PKI certificate management `cert` service will proxy requests to `Vault` previously checking access rights and saving info on successfully created certificate.
|
||||
|
||||
## Development mode
|
||||
If `MF_CERTS_VAULT_HOST` is empty than Development mode is on.
|
||||
|
||||
To issue a certificate:
|
||||
```bash
|
||||
|
||||
TOK=`curl -s --insecure -S -X POST http://localhost/tokens -H 'Content-Type: application/json' -d '{"email":"edge@email.com","password":"12345678"}' | jq -r '.token'`
|
||||
|
||||
curl -s -S -X POST http://localhost:8204/certs -H "Authorization: $TOK" -H 'Content-Type: application/json' -d '{"thing_id":<thing_id>, "rsa_bits":2048, "key_type":"rsa"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ThingID": "",
|
||||
"ClientCert": "-----BEGIN CERTIFICATE-----\nMIIDmTCCAoGgAwIBAgIRANmkAPbTR1UYeYO0Id/4+8gwDQYJKoZIhvcNAQELBQAw\nVzESMBAGA1UEAwwJbG9jYWxob3N0MREwDwYDVQQKDAhNYWluZmx1eDEMMAoGA1UE\nCwwDSW9UMSAwHgYJKoZIhvcNAQkBFhFpbmZvQG1haW5mbHV4LmNvbTAeFw0yMDA2\nMzAxNDIxMDlaFw0yMDA5MjMyMjIxMDlaMFUxETAPBgNVBAoTCE1haW5mbHV4MREw\nDwYDVQQLEwhtYWluZmx1eDEtMCsGA1UEAxMkYjAwZDBhNzktYjQ2YS00NTk3LTli\nNGYtMjhkZGJhNTBjYTYyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\ntgS2fLUWG3CCQz/l6VRQRJfRvWmdxK0mW6zIXGeeOILYZeaLiuiUnohwMJ4RiMqT\nuJbInAIuO/Tt5osfrCFFzPEOLYJ5nZBBaJfTIAxqf84Ou1oeMRll4wpzgeKx0rJO\nXMAARwn1bT9n3uky5QQGSLy4PyyILzSXH/1yCQQctdQB/Ar/UI1TaYoYlGzh7dHT\nWpcxq1HYgCyAtcrQrGD0rEwUn82UBCrnya+bygNqu0oDzIFQwa1G8jxSgXk0mFS1\nWrk7rBipsvp8HQhdnvbEVz4k4AAKcQxesH4DkRx/EXmU2UvN3XysvcJ2bL+UzMNI\njNhAe0pgPbB82F6zkYZ/XQIDAQABo2IwYDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0l\nBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDgQHBAUBAgMEBjAfBgNVHSME\nGDAWgBRs4xR91qEjNRGmw391xS7x6Tc+8jANBgkqhkiG9w0BAQsFAAOCAQEAW/dS\nV4vNLTZwBnPVHUX35pRFxPKvscY+vnnpgyDtITgZHYe0KL+Bs3IHuywtqaezU5x1\nkZo+frE1OcpRvp7HJtDiT06yz+18qOYZMappCWCeAFWtZkMhlvnm3TqTkgui6Xgl\nGj5xnPb15AOlsDE2dkv5S6kEwJGHdVX6AOWfB4ubUq5S9e4ABYzXGUty6Hw/ZUmJ\nhCTRVJ7cQJVTJsl1o7CYT8JBvUUG75LirtoFE4M4JwsfsKZXzrQffTf1ynqI3dN/\nHWySEbvTSWcRcA3MSmOTxGt5/zwCglHDlWPKMrXtjTW7NPuGL5/P9HSB9HGVVeET\nDUMdvYwgj0cUCEu3LA==\n-----END CERTIFICATE-----\n",
|
||||
"IssuingCA": "",
|
||||
"CAChain": null,
|
||||
"ClientKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtgS2fLUWG3CCQz/l6VRQRJfRvWmdxK0mW6zIXGeeOILYZeaL\niuiUnohwMJ4RiMqTuJbInAIuO/Tt5osfrCFFzPEOLYJ5nZBBaJfTIAxqf84Ou1oe\nMRll4wpzgeKx0rJOXMAARwn1bT9n3uky5QQGSLy4PyyILzSXH/1yCQQctdQB/Ar/\nUI1TaYoYlGzh7dHTWpcxq1HYgCyAtcrQrGD0rEwUn82UBCrnya+bygNqu0oDzIFQ\nwa1G8jxSgXk0mFS1Wrk7rBipsvp8HQhdnvbEVz4k4AAKcQxesH4DkRx/EXmU2UvN\n3XysvcJ2bL+UzMNIjNhAe0pgPbB82F6zkYZ/XQIDAQABAoIBAALoal3tqq+/iWU3\npR2oKiweXMxw3oNg3McEKKNJSH7QoFJob3xFoPIzbc9pBxCvY9LEHepYIpL0o8RW\nHqhqU6olg7t4ZSb+Qf1Ax6+wYxctnJCjrO3N4RHSfevqSjr6fEQBEUARSal4JNmr\n0hNUkCEjWrIvrPFMHsn1C5hXR3okJQpGsad4oCGZDp2eZ/NDyvmLBLci9/5CJdRv\n6roOF5ShWweKcz1+pfy666Q8RiUI7H1zXjPaL4yqkv8eg/WPOO0dYF2Ri2Grk9OY\n1qTM0W1vi9zfncinZ0DpgtwMTFQezGwhUyJHSYHmjVBA4AaYIyOQAI/2dl5fXM+O\n9JfXpOUCgYEA10xAtMc/8KOLbHCprpc4pbtOqfchq/M04qPKxQNAjqvLodrWZZgF\nexa+B3eWWn5MxmQMx18AjBCPwbNDK8Rkd9VqzdWempaSblgZ7y1a0rRNTXzN5DFP\noiuRQV4wszCuj5XSdPn+lxApaI/4+TQ0oweIZCpGW39XKePPoB5WZiMCgYEA2G3W\niJncRpmxWwrRPi1W26E9tWOT5s9wYgXWMc+PAVUd/qdDRuMBHpu861Qoghp/MJog\nBYqt2rQqU0OxvIXlXPrXPHXrCLOFwybRCBVREZrg4BZNnjyDTLOu9C+0M3J9ImCh\n3vniYqb7S0gRmoDM0R3Zu4+ajfP2QOGLXw1qHH8CgYEAl0EQ7HBW8V5UYzi7XNcM\nixKOb0YZt83DR74+hC6GujTjeLBfkzw8DX+qvWA8lxLIKVC80YxivAQemryv4h21\nX6Llx/nd1UkXUsI+ZhP9DK5y6I9XroseIRZuk/fyStFWsbVWB6xiOgq2rKkJBzqw\nCCEQpx40E6/gsqNDiIAHvvUCgYBkkjXc6FJ55DWMLuyozfzMtpKsVYeG++InSrsM\nDn1PizQS/7q9mAMPLCOP312rh5CPDy/OI3FCbfI1GwHerwG0QUP/bnQ3aOTBmKoN\n7YnsemIA/5w16bzBycWE5x3/wjXv4aOWr9vJJ/siMm0rtKp4ijyBcevKBxHpeGWB\nWAR1FQKBgGIqAxGnBpip9E24gH894BaGHHMpQCwAxARev6sHKUy27eFUd6ipoTva\n4Wv36iz3gxU4R5B0gyfnxBNiUab/z90cb5+6+FYO13kqjxRRZWffohk5nHlmFN9K\nea7KQHTfTdRhOLUzW2yVqLi9pzfTfA6Yqf3U1YD3bgnWrp1VQnjo\n-----END RSA PRIVATE KEY-----\n",
|
||||
"PrivateKeyType": "",
|
||||
"Serial": "",
|
||||
"Expire": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## PKI mode
|
||||
|
||||
When `MF_CERTS_VAULT_HOST` is set it is presumed that `Vault` is installed and `certs` service will issue certificates using `Vault` API.
|
||||
First you'll need to set up `Vault`.
|
||||
To setup `Vault` follow steps in [Build Your Own Certificate Authority (CA)](https://learn.hashicorp.com/tutorials/vault/pki-engine).
|
||||
|
||||
To setup certs service with `Vault` following environment variables must be set:
|
||||
|
||||
```
|
||||
MF_CERTS_VAULT_HOST=vault-domain.com
|
||||
MF_CERTS_VAULT_PKI_PATH=<vault_pki_path>
|
||||
MF_CERTS_VAULT_ROLE=<vault_role>
|
||||
MF_CERTS_VAULT_TOKEN=<vault_acces_token>
|
||||
```
|
||||
|
||||
For lab purposes you can use docker-compose and script for setting up PKI in [https://github.com/mteodor/vault](https://github.com/mteodor/vault)
|
||||
|
||||
Issuing certificate is same as in **Development** mode.
|
||||
In this mode certificates can also be revoked:
|
||||
|
||||
```bash
|
||||
curl -s -S -X DELETE http://localhost:8204/certs/revoke -H "Authorization: $TOK" -H 'Content-Type: application/json' -d '{"thing_id":"c30b8842-507c-4bcd-973c-74008cef3be5"}'
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of certs service HTTP API.
|
||||
package api
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
)
|
||||
|
||||
func issueCert(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(addCertsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := svc.IssueCert(ctx, req.token, req.ThingID, req.Valid, req.KeyBits, req.KeyType)
|
||||
if err != nil {
|
||||
return certsResponse{Error: err.Error()}, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listCerts(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
page, err := svc.ListCerts(ctx, req.token, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return certsPageRes{
|
||||
Error: err.Error(),
|
||||
}, err
|
||||
}
|
||||
res := certsPageRes{
|
||||
pageRes: pageRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
},
|
||||
Certs: []certsResponse{},
|
||||
}
|
||||
|
||||
for _, cert := range page.Certs {
|
||||
view := certsResponse{
|
||||
Serial: cert.Serial,
|
||||
ThingID: cert.ThingID,
|
||||
}
|
||||
res.Certs = append(res.Certs, view)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func revokeCert(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(revokeReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.RevokeCert(ctx, req.token, req.ThingID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
log "github.com/mainflux/mainflux/logger"
|
||||
)
|
||||
|
||||
var _ certs.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// NewLoggingMiddleware adds logging facilities to the core service.
|
||||
func NewLoggingMiddleware(svc certs.Service, logger log.Logger) certs.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) IssueCert(ctx context.Context, token, thingID, daysValid string, keyBits int, keyType string) (c certs.Cert, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method issue_cert for token: %s and thing: %s took %s to complete", token, 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.IssueCert(ctx, token, thingID, daysValid, keyBits, keyType)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListCerts(ctx context.Context, token string, offset, limit uint64) (cp certs.Page, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_certs for token: %s took %s to complete", token, 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.ListCerts(ctx, token, offset, limit)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RevokeCert(ctx context.Context, token, thingID string) (c certs.Revoke, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method revoke_cert for token: %s and thing: %s took %s to complete", token, 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.RevokeCert(ctx, token, thingID)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
)
|
||||
|
||||
var _ certs.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func MetricsMiddleware(svc certs.Service, counter metrics.Counter, latency metrics.Histogram) certs.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) IssueCert(ctx context.Context, token, thingID string, daysValid string, keyBits int, keyType string) (certs.Cert, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "issue_cert").Add(1)
|
||||
ms.latency.With("method", "issue_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.IssueCert(ctx, token, thingID, daysValid, keyBits, keyType)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListCerts(ctx context.Context, token string, offset, limit uint64) (certs.Page, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_certs").Add(1)
|
||||
ms.latency.With("method", "list_certs").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListCerts(ctx, token, offset, limit)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) RevokeCert(ctx context.Context, token, thingID string) (certs.Revoke, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "revoke_cert").Add(1)
|
||||
ms.latency.With("method", "revoke_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.RevokeCert(ctx, token, thingID)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import "github.com/mainflux/mainflux/certs"
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type addCertsReq struct {
|
||||
token string
|
||||
ThingID string `json:"thing_id"`
|
||||
KeyBits int `json:"key_bits"`
|
||||
KeyType string `json:"key_type"`
|
||||
Valid string `json:"valid"`
|
||||
}
|
||||
|
||||
func (req addCertsReq) validate() error {
|
||||
if req.ThingID == "" && req.token == "" {
|
||||
return errUnauthorized
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
token string
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req *listReq) validate() error {
|
||||
if req.token == "" {
|
||||
return certs.ErrUnauthorizedAccess
|
||||
}
|
||||
if req.limit == 0 || req.limit > maxLimitSize {
|
||||
return certs.ErrMalformedEntity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type revokeReq struct {
|
||||
token string
|
||||
ThingID string `json:"thing_id"`
|
||||
}
|
||||
|
||||
func (req *revokeReq) validate() error {
|
||||
if req.token == "" || req.ThingID == "" {
|
||||
return certs.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type pageRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
}
|
||||
|
||||
type certsPageRes struct {
|
||||
pageRes
|
||||
Certs []certsResponse `json:"certs"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type certsResponse struct {
|
||||
ClientCert map[string]string `json:"client_cert"`
|
||||
ClientKey map[string]string `json:"client_key"`
|
||||
Serial string `json:"serial"`
|
||||
ThingID string `json:"thing_id"`
|
||||
CACert string `json:"ca_cert"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func (res certsPageRes) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res certsPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res certsPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (res certsResponse) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res certsResponse) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res certsResponse) Empty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
offset = "offset"
|
||||
limit = "limit"
|
||||
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
errUnauthorized = errors.New("missing or invalid credentials provided")
|
||||
errInvalidQueryParams = errors.New("invalid query params")
|
||||
errMalformedEntity = errors.New("malformed entity")
|
||||
errConflict = errors.New("entity already exists")
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc certs.Service) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(encodeError),
|
||||
}
|
||||
|
||||
r := bone.New()
|
||||
|
||||
r.Post("/certs", kithttp.NewServer(
|
||||
issueCert(svc),
|
||||
decodeCerts,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/certs", kithttp.NewServer(
|
||||
listCerts(svc),
|
||||
decodeListCerts,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Delete("/certs/revoke", kithttp.NewServer(
|
||||
revokeCert(svc),
|
||||
decodeRevokeCerts,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
r.GetFunc("/version", mainflux.Version("certs"))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
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 decodeListCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
l, err := readUintQuery(r, limit, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o, err := readUintQuery(r, offset, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := listReq{
|
||||
token: r.Header.Get("Authorization"),
|
||||
limit: l,
|
||||
offset: o,
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func readUintQuery(r *http.Request, key string, def uint64) (uint64, error) {
|
||||
vals := bone.GetQuery(r, key)
|
||||
if len(vals) > 1 {
|
||||
return 0, errInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(vals) == 0 {
|
||||
return def, nil
|
||||
}
|
||||
|
||||
strval := vals[0]
|
||||
val, err := strconv.ParseUint(strval, 10, 64)
|
||||
if err != nil {
|
||||
return 0, errInvalidQueryParams
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func decodeCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addCertsReq{token: r.Header.Get("Authorization")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeRevokeCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errUnsupportedContentType
|
||||
}
|
||||
|
||||
req := revokeReq{token: r.Header.Get("Authorization")}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
switch err {
|
||||
case errUnsupportedContentType:
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case io.EOF, errMalformedEntity:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errConflict:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
default:
|
||||
switch err.(type) {
|
||||
case *json.SyntaxError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case *json.UnmarshalTypeError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package certs
|
||||
|
||||
import "context"
|
||||
|
||||
// ConfigsPage contains page related metadata as well as list
|
||||
type Page struct {
|
||||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Certs []Cert
|
||||
}
|
||||
|
||||
// Repository specifies a Config persistence API.
|
||||
type Repository interface {
|
||||
// Save saves cert for thing into database
|
||||
Save(ctx context.Context, cert Cert) (string, error)
|
||||
|
||||
// RetrieveAll retrieve all issued certificates for given owner
|
||||
RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (Page, error)
|
||||
|
||||
// Remove certificate from DB for given thing
|
||||
Remove(ctx context.Context, thingID string) error
|
||||
|
||||
// RetrieveByThing certificate by given thing
|
||||
RetrieveByThing(ctx context.Context, thingID string) (Cert, error)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package certs contains the domain concept definitions needed to support
|
||||
// Mainflux certs service functionality.
|
||||
package certs
|
||||
@@ -0,0 +1,144 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Mainflux Certs service
|
||||
description: HTTP API for Certs service
|
||||
version: "1.0.0"
|
||||
|
||||
paths:
|
||||
/certs:
|
||||
post:
|
||||
summary: Creates a certificate for thing
|
||||
description: Creates a certificate for thing
|
||||
tags:
|
||||
- Thing to proxy
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/CertReq"
|
||||
responses:
|
||||
201:
|
||||
description: Created
|
||||
400:
|
||||
description: Failed due to malformed JSON.
|
||||
500:
|
||||
description: Unexpected server-side error ocurred.
|
||||
/certs/{thingID}:
|
||||
get:
|
||||
summary: Retrieves certificates
|
||||
description: |
|
||||
Retrieves a certificates for given thing ID .
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingID"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/CertsRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to retrieve corresponding certificate.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/certs/revoke:
|
||||
delete:
|
||||
summary: Revokes certificate
|
||||
description: |
|
||||
Revokes a certificates for given thing ID .
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- $ref: "#/components/parameters/ThingID"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/RevokeRes"
|
||||
404:
|
||||
description: |
|
||||
Failed to revoke corresponding certificate.
|
||||
500:
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
description: User's access token. Used instead of credentials in env or config.toml.
|
||||
in: header
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
ThingID:
|
||||
name: thingID
|
||||
description: Thing ID
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
schemas:
|
||||
Certs:
|
||||
type: object
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
description: Corresponding Mainflux Thing ID.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client Certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
issuing_ca:
|
||||
type: string
|
||||
description: CA Certificate that is used to issue client certs, usually intermediate.
|
||||
serial:
|
||||
type: string
|
||||
description: Certificate serial
|
||||
expire:
|
||||
type: string
|
||||
description: Certificate expiry date
|
||||
Revoke:
|
||||
type: object
|
||||
properties:
|
||||
revocation_time:
|
||||
type: string
|
||||
description: Certificate revocation time
|
||||
|
||||
requestBodies:
|
||||
CertReq:
|
||||
description: |
|
||||
Issues a certificate that is required for mTLS. To create a certificate for a thing
|
||||
provide a thing id, data identifying particular thing will be embedded into the Certificate.
|
||||
x509 and ECC certificates are supported when using when Vault is used as PKI.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- thing_id
|
||||
- days_valid
|
||||
- rsa_bits
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
days_valid:
|
||||
type: string
|
||||
rsa_bits:
|
||||
type: integer
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
CertsRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Certs"
|
||||
RevokeRes:
|
||||
description: Certificate revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Revoke"
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package pki wraps vault client
|
||||
package pki
|
||||
|
||||
import "time"
|
||||
|
||||
type Revoke struct {
|
||||
RevocationTime time.Time `mapstructure:"revocation_time"`
|
||||
}
|
||||
|
||||
type Cert struct {
|
||||
ClientCert string `json:"client_cert" mapstructure:"certificate"`
|
||||
IssuingCA string `json:"issuing_ca" mapstructure:"issuing_ca"`
|
||||
CAChain []string `json:"ca_chain" mapstructure:"ca_chain"`
|
||||
ClientKey string `json:"client_key" mapstructure:"private_key"`
|
||||
PrivateKeyType string `json:"private_key_type" mapstructure:"private_key_type"`
|
||||
Serial string `json:"serial" mapstructure:"serial_number"`
|
||||
Expire time.Time `json:"expire" mapstructure:"-"`
|
||||
}
|
||||
|
||||
type Agent interface {
|
||||
// IssueCert issues certificate on PKI
|
||||
IssueCert(cn string, ttl, keyType string, keyBits int) (Cert, error)
|
||||
// Revoke revokes certificate from PKI
|
||||
Revoke(serial string) (Revoke, error)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package pki wraps vault client
|
||||
package pki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const (
|
||||
issue = "issue"
|
||||
revoke = "revoke"
|
||||
apiVer = "v1"
|
||||
)
|
||||
|
||||
var (
|
||||
errFailedVaultCertIssue = errors.New("failed to issue vault certificate")
|
||||
errFailedCertDecoding = errors.New("failed to decode response from vault service")
|
||||
)
|
||||
|
||||
type pkiAgent struct {
|
||||
token string
|
||||
path string
|
||||
role string
|
||||
host string
|
||||
issueURL string
|
||||
revokeURL string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
type certReq struct {
|
||||
CommonName string `json:"common_name"`
|
||||
TTL string `json:"ttl"`
|
||||
KeyBits int `json:"key_bits"`
|
||||
KeyType string `json:"key_type"`
|
||||
}
|
||||
|
||||
type certRevokeReq struct {
|
||||
SerialNumber string `json:"serial_number"`
|
||||
}
|
||||
|
||||
func NewVaultClient(token, host, path, role string) (Agent, error) {
|
||||
conf := &api.Config{
|
||||
Address: host,
|
||||
}
|
||||
|
||||
client, err := api.NewClient(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.SetToken(token)
|
||||
p := pkiAgent{
|
||||
token: token,
|
||||
host: host,
|
||||
role: role,
|
||||
path: path,
|
||||
client: client,
|
||||
issueURL: "/" + apiVer + "/" + path + "/" + issue + "/" + role,
|
||||
revokeURL: "/" + apiVer + "/" + path + "/" + revoke,
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (p *pkiAgent) IssueCert(cn string, ttl, keyType string, keyBits int) (Cert, error) {
|
||||
cReq := certReq{
|
||||
CommonName: cn,
|
||||
TTL: ttl,
|
||||
KeyBits: keyBits,
|
||||
KeyType: keyType,
|
||||
}
|
||||
|
||||
r := p.client.NewRequest("POST", p.issueURL)
|
||||
if err := r.SetJSONBody(cReq); err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
resp, err := p.client.RawRequest(r)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
_, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
return Cert{}, errors.Wrap(errFailedVaultCertIssue, err)
|
||||
}
|
||||
|
||||
s, _ := api.ParseSecret(resp.Body)
|
||||
cert := Cert{}
|
||||
|
||||
if err = mapstructure.Decode(s.Data, &cert); err != nil {
|
||||
return Cert{}, errors.Wrap(errFailedCertDecoding, err)
|
||||
}
|
||||
|
||||
// Expire time calc must be revised value doesnt look correct
|
||||
exp, err := s.Data["expiration"].(json.Number).Float64()
|
||||
if err != nil {
|
||||
return cert, err
|
||||
}
|
||||
expTime := time.Unix(0, int64(exp)*int64(time.Millisecond))
|
||||
cert.Expire = expTime
|
||||
return cert, nil
|
||||
|
||||
}
|
||||
|
||||
func (p *pkiAgent) Revoke(serial string) (Revoke, error) {
|
||||
cReq := certRevokeReq{
|
||||
SerialNumber: serial,
|
||||
}
|
||||
|
||||
r := p.client.NewRequest("POST", p.revokeURL)
|
||||
if err := r.SetJSONBody(cReq); err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
|
||||
resp, err := p.client.RawRequest(r)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
_, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
return Revoke{}, errors.Wrap(errFailedVaultCertIssue, err)
|
||||
}
|
||||
|
||||
s, err := api.ParseSecret(resp.Body)
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
|
||||
rev, err := s.Data["revocation_time"].(json.Number).Float64()
|
||||
if err != nil {
|
||||
return Revoke{}, err
|
||||
}
|
||||
revTime := time.Unix(0, int64(rev)*int64(time.Millisecond))
|
||||
return Revoke{
|
||||
RevocationTime: revTime,
|
||||
}, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
)
|
||||
|
||||
const duplicateErr = "unique_violation"
|
||||
|
||||
var (
|
||||
errSaveDB = errors.New("failed to save certificate to database")
|
||||
errRetrieveDB = errors.New("failed to retrieve certificate from db")
|
||||
errRemove = errors.New("failed to remove certificate from database")
|
||||
errInvalid = "invalid_text_representation"
|
||||
)
|
||||
|
||||
var _ certs.Repository = (*certsRepository)(nil)
|
||||
|
||||
type Cert struct {
|
||||
ThingID string
|
||||
Serial string
|
||||
Expire time.Time
|
||||
}
|
||||
|
||||
type certsRepository struct {
|
||||
db *sqlx.DB
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewRepository instantiates a PostgreSQL implementation of certs
|
||||
// repository.
|
||||
func NewRepository(db *sqlx.DB, log logger.Logger) certs.Repository {
|
||||
return &certsRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr certsRepository) RetrieveAll(ctx context.Context, ownerID string, offset, limit uint64) (certs.Page, error) {
|
||||
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE owner_id = $1 ORDER BY expire LIMIT $2 OFFSET $3;`
|
||||
rows, err := cr.db.Query(q, ownerID, limit, offset)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
|
||||
return certs.Page{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
certificates := []certs.Cert{}
|
||||
|
||||
for rows.Next() {
|
||||
c := certs.Cert{}
|
||||
if err := rows.Scan(&c.ThingID, &c.OwnerID, &c.Serial, &c.Expire); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return certs.Page{}, err
|
||||
|
||||
}
|
||||
certificates = append(certificates, c)
|
||||
}
|
||||
|
||||
q = `SELECT COUNT(*) FROM certs WHERE owner_id = $1`
|
||||
var total uint64
|
||||
if err := cr.db.QueryRow(q, ownerID).Scan(&total); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to count certs due to %s", err))
|
||||
return certs.Page{}, err
|
||||
}
|
||||
|
||||
return certs.Page{
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Certs: certificates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) Save(ctx context.Context, cert certs.Cert) (string, error) {
|
||||
q := `INSERT INTO certs (thing_id, owner_id, serial, expire) VALUES (:thing_id, :owner_id, :serial, :expire)`
|
||||
|
||||
tx, err := cr.db.Beginx()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errSaveDB, err)
|
||||
}
|
||||
|
||||
dbcrt := toDBCert(cert)
|
||||
|
||||
if _, err := tx.NamedExec(q, dbcrt); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
e = errors.New("error conflict")
|
||||
}
|
||||
|
||||
cr.rollback("Failed to insert a Cert", tx, err)
|
||||
|
||||
return "", errors.Wrap(errSaveDB, e)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
cr.rollback("Failed to commit Config save", tx, err)
|
||||
}
|
||||
|
||||
return cert.Serial, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) Remove(ctx context.Context, serial string) error {
|
||||
if _, err := cr.retrieveBySerial(ctx, serial); err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
}
|
||||
q := `DELETE FROM certs WHERE serial = :serial`
|
||||
var c certs.Cert
|
||||
c.Serial = serial
|
||||
dbcrt := toDBCert(c)
|
||||
if _, err := cr.db.NamedExecContext(ctx, q, dbcrt); err != nil {
|
||||
return errors.Wrap(errRemove, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) RetrieveByThing(ctx context.Context, thingID string) (certs.Cert, error) {
|
||||
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE thing_id = $1`
|
||||
var dbcrt dbCert
|
||||
var c certs.Cert
|
||||
|
||||
if err := cr.db.QueryRowxContext(ctx, q, thingID).StructScan(&dbcrt); err != nil {
|
||||
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
|
||||
return c, errors.Wrap(things.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return c, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
c = toCert(dbcrt)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) retrieveBySerial(ctx context.Context, serial string) (certs.Cert, error) {
|
||||
q := `SELECT thing_id, owner_id, serial, expire FROM certs WHERE serial = $1`
|
||||
var dbcrt dbCert
|
||||
var c certs.Cert
|
||||
|
||||
if err := cr.db.QueryRowxContext(ctx, q, serial).StructScan(&dbcrt); err != nil {
|
||||
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if err == sql.ErrNoRows || ok && errInvalid == pqErr.Code.Name() {
|
||||
return c, errors.Wrap(things.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return c, errors.Wrap(errRetrieveDB, err)
|
||||
}
|
||||
c = toCert(dbcrt)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (cr certsRepository) rollback(content string, tx *sqlx.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))
|
||||
}
|
||||
}
|
||||
|
||||
type dbCert struct {
|
||||
ThingID string `db:"thing_id"`
|
||||
Serial string `db:"serial"`
|
||||
Expire time.Time `db:"expire"`
|
||||
OwnerID string `db:"owner_id"`
|
||||
}
|
||||
|
||||
func toDBCert(c certs.Cert) dbCert {
|
||||
return dbCert{
|
||||
ThingID: c.ThingID,
|
||||
OwnerID: c.OwnerID,
|
||||
Serial: c.Serial,
|
||||
Expire: c.Expire,
|
||||
}
|
||||
}
|
||||
|
||||
func toCert(cdb dbCert) certs.Cert {
|
||||
var c certs.Cert
|
||||
c.OwnerID = cdb.OwnerID
|
||||
c.ThingID = cdb.ThingID
|
||||
c.Serial = cdb.Serial
|
||||
c.Expire = cdb.Expire
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2019
|
||||
// Mainflux
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
const primaryKey = "primary_key"
|
||||
|
||||
// ErrMigrate indicates error during database migrations.
|
||||
var ErrMigrate = errors.New("error executing database migrations")
|
||||
|
||||
// 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) (*sqlx.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 := sqlx.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := migrateDB(db); err != nil {
|
||||
mErr, ok := err.(*migrate.TxError)
|
||||
if ok && mErr.Migration.Id == primaryKey {
|
||||
return db, ErrMigrate
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrateDB(db *sqlx.DB) error {
|
||||
migrations := &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "certs_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS certs (
|
||||
thing_id TEXT NOT NULL,
|
||||
owner_id TEXT NOT NULL,
|
||||
expire TIMESTAMPTZ NOT NULL,
|
||||
serial TEXT NOT NULL,
|
||||
PRIMARY KEY (thing_id, owner_id)
|
||||
);`,
|
||||
},
|
||||
Down: []string{
|
||||
"DROP TABLE IF EXISTS certs;",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := migrate.Exec(db.DB, "postgres", migrations, migrate.Up)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux/certs/postgres"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
testLog, _ = logger.New(os.Stdout, logger.Info.String())
|
||||
db *sqlx.DB
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
cfg := []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.2-alpine", cfg)
|
||||
if err != nil {
|
||||
testLog.Error(fmt.Sprintf("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 = sqlx.Open("postgres", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
testLog.Error(fmt.Sprintf("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 {
|
||||
testLog.Error(fmt.Sprintf("Could not setup test DB connection: %s", err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not purge container: %s", err))
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package certs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/certs/pki"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
errFailedKeyCreation = errors.New("failed to create client private key")
|
||||
errFailedDateSetting = errors.New("failed to set date for certificate")
|
||||
errKeyBitsValueWrong = errors.New("missing RSA bits for certificate creation")
|
||||
errMissingCACertificate = errors.New("missing CA certificate for certificate signing")
|
||||
errFailedSerialGeneration = errors.New("failed to generate certificate serial")
|
||||
errFailedPemKeyWrite = errors.New("failed to write PEM key")
|
||||
errFailedPemDataWrite = errors.New("failed to write pem data for certificate")
|
||||
errPrivateKeyUnsupportedType = errors.New("private key type is unsupported")
|
||||
errPrivateKeyEmpty = errors.New("private key is empty")
|
||||
errFailedToRemoveCertFromDB = errors.New("failed to remove cert serial from db")
|
||||
errFailedCertCreation = errors.New("failed to create client certificate")
|
||||
errFailedCertRevocation = errors.New("failed to revoke certificate")
|
||||
)
|
||||
|
||||
var _ Service = (*certsService)(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 {
|
||||
// IssueCert issues certificate for given thing id if access is granted with token
|
||||
IssueCert(ctx context.Context, token, thingID, daysValid string, keyBits int, keyType string) (Cert, error)
|
||||
|
||||
// ListCerts lists all certificates issued for given owner
|
||||
ListCerts(ctx context.Context, token string, offset, limit uint64) (Page, error)
|
||||
|
||||
// RevokeCert revokes certificate for given thing
|
||||
RevokeCert(ctx context.Context, token, thingID string) (Revoke, error)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
LogLevel string
|
||||
ClientTLS bool
|
||||
CaCerts string
|
||||
HTTPPort string
|
||||
ServerCert string
|
||||
ServerKey string
|
||||
BaseURL string
|
||||
ThingsPrefix string
|
||||
JaegerURL string
|
||||
AuthnURL string
|
||||
AuthnTimeout time.Duration
|
||||
SignTLSCert tls.Certificate
|
||||
SignX509Cert *x509.Certificate
|
||||
SignRSABits int
|
||||
SignHoursValid string
|
||||
PKIHost string
|
||||
PKIPath string
|
||||
PKIRole string
|
||||
PKIToken string
|
||||
}
|
||||
|
||||
type certsService struct {
|
||||
auth mainflux.AuthNServiceClient
|
||||
certsRepo Repository
|
||||
sdk mfsdk.SDK
|
||||
conf Config
|
||||
pki pki.Agent
|
||||
}
|
||||
|
||||
// New returns new Certs service.
|
||||
func New(auth mainflux.AuthNServiceClient, certs Repository, sdk mfsdk.SDK, config Config, pki pki.Agent) Service {
|
||||
return &certsService{
|
||||
certsRepo: certs,
|
||||
sdk: sdk,
|
||||
auth: auth,
|
||||
conf: config,
|
||||
pki: pki,
|
||||
}
|
||||
}
|
||||
|
||||
type Revoke struct {
|
||||
RevocationTime time.Time `mapstructure:"revocation_time"`
|
||||
}
|
||||
|
||||
type Cert struct {
|
||||
OwnerID string `json:"owner_id" mapstructure:"owner_id"`
|
||||
ThingID string `json:"thing_id" mapstructure:"thing_id"`
|
||||
ClientCert string `json:"client_cert" mapstructure:"certificate"`
|
||||
IssuingCA string `json:"issuing_ca" mapstructure:"issuing_ca"`
|
||||
CAChain []string `json:"ca_chain" mapstructure:"ca_chain"`
|
||||
ClientKey string `json:"client_key" mapstructure:"private_key"`
|
||||
PrivateKeyType string `json:"private_key_type" mapstructure:"private_key_type"`
|
||||
Serial string `json:"serial" mapstructure:"serial_number"`
|
||||
Expire time.Time `json:"expire" mapstructure:"-"`
|
||||
}
|
||||
|
||||
func (cs *certsService) IssueCert(ctx context.Context, token, thingID string, daysValid string, keyBits int, keyType string) (Cert, error) {
|
||||
var c Cert
|
||||
owner, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return c, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
thing, err := cs.sdk.Thing(thingID, token)
|
||||
if err != nil {
|
||||
return c, errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
|
||||
// If PKIHost is not set we don't use 3rd party PKI service.
|
||||
if cs.conf.PKIHost == "" {
|
||||
c.ClientCert, c.ClientKey, err = cs.certs(thing.Key, daysValid, keyBits)
|
||||
if err != nil {
|
||||
return c, errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
cert, err := cs.pki.IssueCert(thingID, daysValid, keyType, keyBits)
|
||||
if err != nil {
|
||||
return c, errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
|
||||
c.ThingID = thingID
|
||||
c.OwnerID = owner.GetValue()
|
||||
c.ClientCert = cert.ClientCert
|
||||
c.IssuingCA = cert.IssuingCA
|
||||
c.CAChain = cert.CAChain
|
||||
c.ClientKey = cert.ClientKey
|
||||
c.PrivateKeyType = cert.PrivateKeyType
|
||||
c.Serial = cert.Serial
|
||||
c.Expire = cert.Expire
|
||||
|
||||
_, err = cs.certsRepo.Save(context.Background(), c)
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (cs *certsService) RevokeCert(ctx context.Context, token, thingID string) (Revoke, error) {
|
||||
var revoke Revoke
|
||||
_, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
thing, err := cs.sdk.Thing(thingID, token)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(errFailedCertRevocation, err)
|
||||
}
|
||||
|
||||
cert, err := cs.certsRepo.RetrieveByThing(ctx, thing.ID)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(errFailedCertRevocation, err)
|
||||
}
|
||||
|
||||
r, err := cs.pki.Revoke(cert.Serial)
|
||||
if err != nil {
|
||||
return revoke, errors.Wrap(errFailedCertRevocation, err)
|
||||
}
|
||||
revoke.RevocationTime = r.RevocationTime
|
||||
if err = cs.certsRepo.Remove(context.Background(), cert.Serial); err != nil {
|
||||
return revoke, errors.Wrap(errFailedToRemoveCertFromDB, err)
|
||||
}
|
||||
return revoke, nil
|
||||
}
|
||||
|
||||
func (cs *certsService) ListCerts(ctx context.Context, token string, offset, limit uint64) (Page, error) {
|
||||
u, err := cs.auth.Identify(ctx, &mainflux.Token{Value: token})
|
||||
if err != nil {
|
||||
return Page{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
||||
}
|
||||
|
||||
return cs.certsRepo.RetrieveAll(ctx, u.GetValue(), offset, limit)
|
||||
}
|
||||
|
||||
func (cs *certsService) certs(thingKey, daysValid string, keyBits int) (string, string, error) {
|
||||
if cs.conf.SignX509Cert == nil {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, errMissingCACertificate)
|
||||
}
|
||||
if keyBits == 0 {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, errKeyBitsValueWrong)
|
||||
}
|
||||
var priv interface{}
|
||||
priv, err := rsa.GenerateKey(rand.Reader, keyBits)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedKeyCreation, err)
|
||||
}
|
||||
|
||||
if daysValid == "" {
|
||||
daysValid = cs.conf.SignHoursValid
|
||||
}
|
||||
|
||||
notBefore := time.Now()
|
||||
validFor, err := time.ParseDuration(daysValid)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedDateSetting, err)
|
||||
}
|
||||
notAfter := notBefore.Add(validFor)
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedSerialGeneration, err)
|
||||
}
|
||||
|
||||
tmpl := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Mainflux"},
|
||||
CommonName: thingKey,
|
||||
OrganizationalUnit: []string{"mainflux"},
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
SubjectKeyId: []byte{1, 2, 3, 4, 6},
|
||||
}
|
||||
|
||||
pubKey, err := publicKey(priv)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, cs.conf.SignX509Cert, pubKey, cs.conf.SignTLSCert.PrivateKey)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedCertCreation, err)
|
||||
}
|
||||
|
||||
var bw, keyOut bytes.Buffer
|
||||
buffWriter := bufio.NewWriter(&bw)
|
||||
buffKeyOut := bufio.NewWriter(&keyOut)
|
||||
|
||||
if err := pem.Encode(buffWriter, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
return "", "", errors.Wrap(errFailedPemDataWrite, err)
|
||||
}
|
||||
buffWriter.Flush()
|
||||
cert := bw.String()
|
||||
|
||||
block, err := pemBlockForKey(priv)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(errFailedPemKeyWrite, err)
|
||||
}
|
||||
if err := pem.Encode(buffKeyOut, block); err != nil {
|
||||
return "", "", errors.Wrap(errFailedPemKeyWrite, err)
|
||||
}
|
||||
buffKeyOut.Flush()
|
||||
key := keyOut.String()
|
||||
|
||||
return cert, key, nil
|
||||
}
|
||||
|
||||
func publicKey(priv interface{}) (interface{}, error) {
|
||||
if priv == nil {
|
||||
return nil, errPrivateKeyEmpty
|
||||
}
|
||||
switch k := priv.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
default:
|
||||
return nil, errPrivateKeyUnsupportedType
|
||||
}
|
||||
}
|
||||
|
||||
func pemBlockForKey(priv interface{}) (*pem.Block, error) {
|
||||
switch k := priv.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
b, err := x509.MarshalECPrivateKey(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
+159
-25
@@ -1,108 +1,242 @@
|
||||
# Mainflux CLI
|
||||
## Build
|
||||
From the project root:
|
||||
```
|
||||
```bash
|
||||
make cli
|
||||
```
|
||||
|
||||
## Usage
|
||||
### Service
|
||||
#### Get the version of Mainflux services
|
||||
```
|
||||
```bash
|
||||
mainflux-cli version
|
||||
```
|
||||
|
||||
### Users management
|
||||
#### Create User
|
||||
```
|
||||
mainflux-cli users create john.doe@email.com password
|
||||
```bash
|
||||
mainflux-cli users create <user_email> <user_password>
|
||||
```
|
||||
|
||||
#### Login User
|
||||
```bash
|
||||
mainflux-cli users token <user_email> <user_password>
|
||||
```
|
||||
mainflux-cli users token john.doe@email.com password
|
||||
|
||||
#### Retrieve User
|
||||
```bash
|
||||
mainflux-cli users get <user_auth_token>
|
||||
```
|
||||
|
||||
#### Update User Metadata
|
||||
```bash
|
||||
mainflux-cli users update '{"key1":"value1", "key2":"value2"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Update User Password
|
||||
```bash
|
||||
mainflux-cli users password <old_password> <password> <user_auth_token>
|
||||
```
|
||||
|
||||
### System Provisioning
|
||||
#### Create Thing (type Device)
|
||||
```
|
||||
mainflux-cli things create '{"name":"myDevice"}' <user_auth_token>
|
||||
#### Create Thing
|
||||
```bash
|
||||
mainflux-cli things create '{"name":"myThing"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Create Thing (type Application)
|
||||
#### Create Thing with metadata
|
||||
```bash
|
||||
mainflux-cli things create '{"name":"myThing", "metadata": {\"key1\":\"value1\"}}' <user_auth_token>
|
||||
```
|
||||
mainflux-cli things create '{"name":"myDevice"}' <user_auth_token>
|
||||
|
||||
#### Bulk Provision Things
|
||||
```bash
|
||||
mainflux-cli provision things <file> <user_auth_token>
|
||||
```
|
||||
|
||||
* `file` - A CSV or JSON file containing things
|
||||
* `user_auth_token` - A valid user auth token for the current system
|
||||
|
||||
#### Update Thing
|
||||
```
|
||||
```bash
|
||||
mainflux-cli things update '{"id":"<thing_id>", "name":"myNewName"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Remove Thing
|
||||
```
|
||||
```bash
|
||||
mainflux-cli things delete <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of provisioned Things
|
||||
```
|
||||
```bash
|
||||
mainflux-cli things get all --offset=1 --limit=5 <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve Thing By ID
|
||||
```
|
||||
```bash
|
||||
mainflux-cli things get <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Create Channel
|
||||
```
|
||||
```bash
|
||||
mainflux-cli channels create '{"name":"myChannel"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Update Channel
|
||||
#### Bulk Provision Channels
|
||||
```bash
|
||||
mainflux-cli provision channels <file> <user_auth_token>
|
||||
```
|
||||
mainflux-cli channels update '{"id":"<channel_id>","name":"myNewName"}' <user_auth_token>
|
||||
|
||||
* `file` - A CSV or JSON file containing channels
|
||||
* `user_auth_token` - A valid user auth token for the current system
|
||||
|
||||
#### Update Channel
|
||||
```bash
|
||||
mainflux-cli channels update '{"id":"<channel_id>","name":"myNewName"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Remove Channel
|
||||
```
|
||||
```bash
|
||||
mainflux-cli channels delete <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of provisioned Channels
|
||||
```
|
||||
```bash
|
||||
mainflux-cli channels get all --offset=1 --limit=5 <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve Channel By ID
|
||||
```
|
||||
```bash
|
||||
mainflux-cli channels get <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
### Access control
|
||||
#### Connect Thing to Channel
|
||||
```
|
||||
```bash
|
||||
mainflux-cli things connect <thing_id> <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Disconnect Thing from Channel
|
||||
#### Bulk Connect Things to Channels
|
||||
```bash
|
||||
mainflux-cli provision connect <file> <user_auth_token>
|
||||
```
|
||||
|
||||
* `file` - A CSV or JSON file containing thing and channel ids
|
||||
* `user_auth_token` - A valid user auth token for the current system
|
||||
|
||||
An example CSV file might be
|
||||
|
||||
```csv
|
||||
<thing_id>,<channel_id>
|
||||
<thing_id>,<channel_id>
|
||||
```
|
||||
|
||||
in which the first column is thing IDs and the second column is channel IDs. A connection will be created for each thing to each channel. This example would result in 4 connections being created.
|
||||
|
||||
A comparable JSON file would be
|
||||
|
||||
```json
|
||||
{
|
||||
"thing_ids": [
|
||||
"<thing_id>",
|
||||
"<thing_id>"
|
||||
],
|
||||
"channel_ids": [
|
||||
"<channel_id>",
|
||||
"<channel_id>"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Disconnect Thing from Channel
|
||||
```bash
|
||||
mainflux-cli things disconnect <thing_id> <channel_id> <user_auth_token>
|
||||
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of Channels connected to Thing
|
||||
```
|
||||
```bash
|
||||
mainflux-cli things connections <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Retrieve a subset list of Things connected to Channel
|
||||
```
|
||||
```bash
|
||||
mainflux-cli channels connections <channel_id> <user_auth_token>
|
||||
```
|
||||
|
||||
|
||||
### Messaging
|
||||
#### Send a message over HTTP
|
||||
```
|
||||
```bash
|
||||
mainflux-cli messages 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>
|
||||
```
|
||||
|
||||
#### Read messages over HTTP
|
||||
```bash
|
||||
mainflux-cli messages read <channel_id> <thing_auth_token>
|
||||
```
|
||||
|
||||
### Bootstrap
|
||||
|
||||
#### Add configuration
|
||||
```bash
|
||||
mainflux-cli bootstrap add '{"external_id": "myExtID", "external_key": "myExtKey", "name": "myName", "content": "myContent"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### View configuration
|
||||
```bash
|
||||
mainflux-cli bootstrap view <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Update configuration
|
||||
```bash
|
||||
mainflux-cli bootstrap update '{"MFThing":"<thing_id>", "name": "newName", "content": "newContent"}' <user_auth_token>
|
||||
```
|
||||
|
||||
#### Remove configuration
|
||||
```bash
|
||||
mainflux-cli bootstrap remove <thing_id> <user_auth_token>
|
||||
```
|
||||
|
||||
#### Bootstrap configuration
|
||||
```bash
|
||||
mainflux-cli bootstrap bootstrap <external_id> <external_key>
|
||||
```
|
||||
|
||||
### Groups
|
||||
#### Create new group
|
||||
```bash
|
||||
mainflux-cli groups create '{"name":"<group_name>","parent_id":"<parent_group_id>","description":"<description>","metadata":{"key":"value",...}}' <user_auth_token>
|
||||
```
|
||||
#### Delete group
|
||||
```bash
|
||||
mainflux-cli groups delete <group_id> <user_auth_token>
|
||||
```
|
||||
#### Get group with id
|
||||
```bash
|
||||
mainflux-cli groups get <group_id> <user_auth_token>
|
||||
```
|
||||
#### List all groups
|
||||
```bash
|
||||
mainflux-cli groups get all <user_auth_token>
|
||||
```
|
||||
#### List children groups for some group
|
||||
```bash
|
||||
mainflux-cli groups get children <parent_group_id> <user_auth_token>
|
||||
```
|
||||
#### Assign user to a group
|
||||
```bash
|
||||
mainflux-cli groups assign <user_id> <group_id> <user_auth_token>
|
||||
```
|
||||
#### Unassign user from group
|
||||
```bash
|
||||
mainflux-cli groups unassign <user_id> <group_id> <user_auth_token>
|
||||
```
|
||||
#### List users for a group
|
||||
```bash
|
||||
mainflux-cli groups members <group_id> <user_auth_token>
|
||||
```
|
||||
#### List groups that user belongs to
|
||||
```bash
|
||||
mainflux-cli groups membership <user_id> <user_auth_token>
|
||||
```
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdBootstrap = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "add",
|
||||
Short: "add <JSON_config> <user_auth_token>",
|
||||
Long: `Adds new Thing Bootstrap Config to the user identified by the provided key`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
var cfg mfxsdk.BootstrapConfig
|
||||
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := sdk.AddBootstrap(args[1], cfg)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logCreated(id)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "view",
|
||||
Short: "view <thing_id> <user_auth_token>",
|
||||
Long: `Returns Thing Config with given ID belonging to the user identified by the given key`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := sdk.ViewBootstrap(args[1], args[0])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(c)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update <JSON_config> <user_auth_token>",
|
||||
Long: `Updates editable fields of the provided Config`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
var cfg mfxsdk.BootstrapConfig
|
||||
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.UpdateBootstrap(args[1], cfg); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "remove <thing_id> <user_auth_token>",
|
||||
Long: `Removes Config with specified key that belongs to the user identified by the given key`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.RemoveBootstrap(args[1], args[0]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "bootstrap",
|
||||
Short: "bootstrap <external_id> <external_key>",
|
||||
Long: `Returns Config to the Thing with provided external ID using external key`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := sdk.Bootstrap(args[1], args[0])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(c)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewBootstrapCmd returns bootstrap command.
|
||||
func NewBootstrapCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "bootstrap",
|
||||
Short: "Bootstrap management",
|
||||
Long: `Bootstrap management: create, get, update or delete Bootstrap config`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("bootstrap [add | view | update | remove | bootstrap]")
|
||||
},
|
||||
}
|
||||
|
||||
for i := range cmdBootstrap {
|
||||
cmd.AddCommand(&cmdBootstrap[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdCerts = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "issue",
|
||||
Short: "issue <thing_id> <keybits> <keytype> <hoursvalid> <user_auth_token>",
|
||||
Long: `Issues new certificate for a thing`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 5 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
thingID := args[0]
|
||||
keyBits, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
logError(errors.New("invalid format for keybits"))
|
||||
return
|
||||
}
|
||||
|
||||
keyType := args[2]
|
||||
valid := args[3]
|
||||
token := args[4]
|
||||
|
||||
c, err := sdk.IssueCert(thingID, keyBits, keyType, valid, token)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(c)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewCertsCmd returns certificate command.
|
||||
func NewCertsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "cert",
|
||||
Short: "Certificate management",
|
||||
Long: `Certificate management: create certificates for things"`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("cert issue <thing_id> <keybits> <keytype> <hoursvalid> <user_auth_token>")
|
||||
},
|
||||
}
|
||||
|
||||
for i := range cmdCerts {
|
||||
cmd.AddCommand(&cmdCerts[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
+24
-5
@@ -6,7 +6,7 @@ package cli
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -111,14 +111,33 @@ var cmdChannels = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "connections",
|
||||
Short: "connections <channel_id> <user_auth_token>",
|
||||
Long: `List of Things connected to Channel`,
|
||||
Long: `List of Things connected to a 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))
|
||||
cl, err := sdk.ThingsByChannel(args[1], args[0], uint64(Offset), uint64(Limit), false)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(cl)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "not-connected",
|
||||
Short: "not-connected <channel_id> <user_auth_token>",
|
||||
Long: `List of Things not connected to a 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), false)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
@@ -134,9 +153,9 @@ func NewChannelsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "channels",
|
||||
Short: "Channels management",
|
||||
Long: `Channels management: create, get, update or delete Channel and get list of Things connected to Channel`,
|
||||
Long: `Channels management: create, get, update or delete Channel and get list of Things connected or not connected to a Channel`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("channels [create | get | update | delete | connections]")
|
||||
logUsage("channels [create | get | update | delete | connections | not-connected]")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdGroups = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "create",
|
||||
Short: "create <JSON_group> <user_auth_token>",
|
||||
Long: `Creates new group
|
||||
JSON_group:
|
||||
{
|
||||
"Name":<group_name>,
|
||||
"Description":<description>,
|
||||
"ParentID":<parent_id>,
|
||||
"Metadata":<metadata>,
|
||||
}
|
||||
Name - is unique group name
|
||||
ParentID - ID of a group that is a parent to the creating group
|
||||
Metadata - JSON structured string`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
var group mfxsdk.Group
|
||||
if err := json.Unmarshal([]byte(args[0]), &group); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
id, err := sdk.CreateGroup(group, args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logCreated(id)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "get",
|
||||
Short: "get [all | children <group_id> | group_id] <user_auth_token>",
|
||||
Long: `Get all users groups, group children or group by id.
|
||||
all - lists all groups
|
||||
children <group_id> - lists all children groups of <group_id>
|
||||
<group_id> - shows group with provided group ID`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
if args[0] == "all" {
|
||||
l, err := sdk.Groups(args[1], uint64(Offset), uint64(Limit), "")
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(l)
|
||||
return
|
||||
}
|
||||
if args[0] == "children" {
|
||||
l, err := sdk.Groups(args[2], uint64(Offset), uint64(Limit), args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(l)
|
||||
return
|
||||
}
|
||||
t, err := sdk.Group(args[0], args[1])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(t)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "assign",
|
||||
Short: "assign <user_id> <group_id> <user_auth_token>",
|
||||
Long: `Assign user to a group.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
if err := sdk.Assign(args[0], args[1], args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "unassign",
|
||||
Short: "unassign <user_id> <group_id> <user_auth_token>",
|
||||
Long: `Unassign user from a group.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
if err := sdk.Unassign(args[0], args[1], args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "delete <group_id> <user_auth_token>",
|
||||
Long: `Delete users group.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
if err := sdk.DeleteGroup(args[0], args[1]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "members",
|
||||
Short: "members <group_id> <user_auth_token>",
|
||||
Long: `Lists all user members of a group.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
up, err := sdk.Members(args[0], args[1], uint64(Offset), uint64(Limit))
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(up)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "membership",
|
||||
Short: "membership <user_id> <user_auth_token>",
|
||||
Long: `List user groups membership`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
up, err := sdk.Memberships(args[0], args[1], uint64(Offset), uint64(Limit))
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
logJSON(up)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewGroupsCmd returns users command.
|
||||
func NewGroupsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "groups",
|
||||
Short: "Groups management",
|
||||
Long: `Groups management: create groups and assigns user to groups"`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("Usage: Groups [create | get | delete | assign | unassign | members | membership]")
|
||||
},
|
||||
}
|
||||
for i := range cmdGroups {
|
||||
cmd.AddCommand(&cmdGroups[i])
|
||||
}
|
||||
return &cmd
|
||||
}
|
||||
+18
-17
@@ -13,7 +13,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/docker/pkg/namesgenerator"
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -122,9 +122,9 @@ var cmdProvision = []cobra.Command{
|
||||
// Create test user
|
||||
user := mfxsdk.User{
|
||||
Email: un,
|
||||
Password: "123",
|
||||
Password: "12345678",
|
||||
}
|
||||
if err := sdk.CreateUser(user); err != nil {
|
||||
if _, err := sdk.CreateUser(user); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
@@ -168,21 +168,22 @@ var cmdProvision = []cobra.Command{
|
||||
}
|
||||
|
||||
// Connect things to channels - first thing to both channels, second only to first
|
||||
for i := 0; i < numThings; i++ {
|
||||
if err := sdk.ConnectThing(things[i].ID, channels[i].ID, ut); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
conIDs := mfxsdk.ConnectionIDs{
|
||||
ChannelIDs: []string{channels[0].ID, channels[1].ID},
|
||||
ThingIDs: []string{things[0].ID},
|
||||
}
|
||||
if err := sdk.Connect(conIDs, ut); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if i%2 == 0 {
|
||||
if i+1 >= len(channels) {
|
||||
break
|
||||
}
|
||||
if err := sdk.ConnectThing(things[i].ID, channels[i+1].ID, ut); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
conIDs = mfxsdk.ConnectionIDs{
|
||||
ChannelIDs: []string{channels[0].ID},
|
||||
ThingIDs: []string{things[1].ID},
|
||||
}
|
||||
if err := sdk.Connect(conIDs, ut); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(user, ut, things, channels)
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
package cli
|
||||
|
||||
import mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
import mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
|
||||
// Keep SDK handle in global var
|
||||
var sdk mfxsdk.SDK
|
||||
|
||||
+25
-9
@@ -6,12 +6,10 @@ package cli
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const thingsEP = "things"
|
||||
|
||||
var cmdThings = []cobra.Command{
|
||||
cobra.Command{
|
||||
Use: "create",
|
||||
@@ -120,10 +118,9 @@ var cmdThings = []cobra.Command{
|
||||
}
|
||||
|
||||
connIDs := mfxsdk.ConnectionIDs{
|
||||
[]string{args[0]},
|
||||
[]string{args[1]},
|
||||
ChannelIDs: []string{args[1]},
|
||||
ThingIDs: []string{args[0]},
|
||||
}
|
||||
|
||||
if err := sdk.Connect(connIDs, args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
@@ -160,7 +157,26 @@ var cmdThings = []cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
cl, err := sdk.ChannelsByThing(args[1], args[0], uint64(Offset), uint64(Limit))
|
||||
cl, err := sdk.ChannelsByThing(args[1], args[0], uint64(Offset), uint64(Limit), true)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(cl)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "not-connected",
|
||||
Short: "not-connected <thing_id> <user_auth_token>",
|
||||
Long: `List of Channels not connected to a 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), false)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
@@ -176,9 +192,9 @@ func NewThingsCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "things",
|
||||
Short: "Things management",
|
||||
Long: `Things management: create, get, update or delete Thing, connect or disconnect Thing from Channel and get the list of Channels connected to Thing`,
|
||||
Long: `Things management: create, get, update or delete Thing, connect or disconnect Thing from Channel and get the list of Channels connected or disconnected from a Thing`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("things [create | get | update | delete | connect | disconnect | connections]")
|
||||
logUsage("things [create | get | update | delete | connect | disconnect | connections | not-connected]")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+68
-4
@@ -4,7 +4,9 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
mfxsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
"encoding/json"
|
||||
|
||||
mfxsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -23,12 +25,32 @@ var cmdUsers = []cobra.Command{
|
||||
Email: args[0],
|
||||
Password: args[1],
|
||||
}
|
||||
if err := sdk.CreateUser(user); err != nil {
|
||||
id, err := sdk.CreateUser(user)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
logCreated(id)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "get",
|
||||
Short: "get <user_auth_token>",
|
||||
Long: `Returns user object`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
u, err := sdk.User(args[0])
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSON(u)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
@@ -54,6 +76,48 @@ var cmdUsers = []cobra.Command{
|
||||
logCreated(token)
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "update",
|
||||
Short: "update <JSON_string> <user_auth_token>",
|
||||
Long: `Update user metadata`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
var user mfxsdk.User
|
||||
if err := json.Unmarshal([]byte(args[0]), &user.Metadata); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.UpdateUser(user, args[1]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
cobra.Command{
|
||||
Use: "password",
|
||||
Short: "password <old_password> <password> <user_auth_token>",
|
||||
Long: `Update user password`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsage(cmd.Short)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.UpdatePassword(args[0], args[1], args[2]); err != nil {
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
|
||||
logOK()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewUsersCmd returns users command.
|
||||
@@ -63,7 +127,7 @@ func NewUsersCmd() *cobra.Command {
|
||||
Short: "Users management",
|
||||
Long: `Users management: create accounts and tokens"`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logUsage("Usage: users [create | token]")
|
||||
logUsage("Usage: users [create | get | update | token | password]")
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -21,8 +21,8 @@ import (
|
||||
"github.com/mainflux/mainflux/authn/jwt"
|
||||
"github.com/mainflux/mainflux/authn/postgres"
|
||||
"github.com/mainflux/mainflux/authn/tracing"
|
||||
mfidp "github.com/mainflux/mainflux/authn/uuid"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
@@ -36,7 +36,7 @@ const (
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "authn"
|
||||
defDB = "authn"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
@@ -53,7 +53,7 @@ const (
|
||||
envDBPort = "MF_AUTHN_DB_PORT"
|
||||
envDBUser = "MF_AUTHN_DB_USER"
|
||||
envDBPass = "MF_AUTHN_DB_PASS"
|
||||
envDBName = "MF_AUTHN_DB"
|
||||
envDB = "MF_AUTHN_DB"
|
||||
envDBSSLMode = "MF_AUTHN_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_AUTHN_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_AUTHN_DB_SSL_KEY"
|
||||
@@ -122,7 +122,7 @@ func loadConfig() config {
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Name: mainflux.Env(envDBName, defDBName),
|
||||
Name: mainflux.Env(envDB, defDB),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
@@ -179,9 +179,9 @@ func newService(db *sqlx.DB, tracer opentracing.Tracer, secret string, logger lo
|
||||
database := postgres.NewDatabase(db)
|
||||
repo := tracing.New(postgres.New(database), tracer)
|
||||
|
||||
idp := mfidp.New()
|
||||
up := uuidProvider.New()
|
||||
t := jwt.New(secret)
|
||||
svc := authn.New(repo, idp, t)
|
||||
svc := authn.New(repo, up, t)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
|
||||
+18
-18
@@ -30,7 +30,7 @@ import (
|
||||
api "github.com/mainflux/mainflux/bootstrap/api"
|
||||
"github.com/mainflux/mainflux/bootstrap/postgres"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
mfsdk "github.com/mainflux/mainflux/sdk/go"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
"google.golang.org/grpc"
|
||||
@@ -43,7 +43,7 @@ const (
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "bootstrap"
|
||||
defDB = "bootstrap"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
@@ -64,15 +64,15 @@ const (
|
||||
defESDB = "0"
|
||||
defESConsumerName = "bootstrap"
|
||||
defJaegerURL = ""
|
||||
defAuthURL = "localhost:8181"
|
||||
defAuthTimeout = "1" // in seconds
|
||||
defAuthnURL = "localhost:8181"
|
||||
defAuthnTimeout = "1s"
|
||||
|
||||
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"
|
||||
envDB = "MF_BOOTSTRAP_DB"
|
||||
envDBSSLMode = "MF_BOOTSTRAP_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_BOOTSTRAP_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_BOOTSTRAP_DB_SSL_KEY"
|
||||
@@ -93,8 +93,8 @@ const (
|
||||
envESDB = "MF_BOOTSTRAP_ES_DB"
|
||||
envESConsumerName = "MF_BOOTSTRAP_EVENT_CONSUMER"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envAuthURL = "MF_AUTH_URL"
|
||||
envAuthTimeout = "MF_AUTH_TIMEOUT"
|
||||
envAuthnURL = "MF_AUTHN_GRPC_URL"
|
||||
envAuthnTimeout = "MF_AUTHN_GRPC_TIMEOUT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
@@ -116,8 +116,8 @@ type config struct {
|
||||
esDB string
|
||||
esConsumerName string
|
||||
jaegerURL string
|
||||
authURL string
|
||||
authTimeout time.Duration
|
||||
authnURL string
|
||||
authnTimeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -143,7 +143,7 @@ func main() {
|
||||
authConn := connectToAuth(cfg, logger)
|
||||
defer authConn.Close()
|
||||
|
||||
auth := authapi.NewClient(authTracer, authConn, cfg.authTimeout)
|
||||
auth := authapi.NewClient(authTracer, authConn, cfg.authnTimeout)
|
||||
|
||||
svc := newService(auth, db, logger, esClient, cfg)
|
||||
errs := make(chan error, 2)
|
||||
@@ -171,16 +171,16 @@ func loadConfig() config {
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Name: mainflux.Env(envDBName, defDBName),
|
||||
Name: mainflux.Env(envDB, defDB),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envAuthTimeout, defAuthTimeout), 10, 64)
|
||||
authnTimeout, err := time.ParseDuration(mainflux.Env(envAuthnTimeout, defAuthnTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envAuthTimeout, err.Error())
|
||||
log.Fatalf("Invalid %s value: %s", envAuthnTimeout, err.Error())
|
||||
}
|
||||
encKey, err := hex.DecodeString(mainflux.Env(envEncryptKey, defEncryptKey))
|
||||
if err != nil {
|
||||
@@ -212,8 +212,8 @@ func loadConfig() config {
|
||||
esDB: mainflux.Env(envESDB, defESDB),
|
||||
esConsumerName: mainflux.Env(envESConsumerName, defESConsumerName),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
authURL: mainflux.Env(envAuthURL, defAuthURL),
|
||||
authTimeout: time.Duration(timeout) * time.Second,
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: authnTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,9 +311,9 @@ func connectToAuth(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.authURL, opts...)
|
||||
conn, err := grpc.Dial(cfg.authnURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
logger.Error(fmt.Sprintf("Failed to connect to authn service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -336,6 +336,6 @@ func subscribeToThingsES(svc bootstrap.Service, client *r.Client, consumer strin
|
||||
eventStore := rediscons.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))
|
||||
logger.Warn(fmt.Sprintf("Bootstrap service failed to subscribe to event sourcing: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,42 +34,48 @@ import (
|
||||
const (
|
||||
sep = ","
|
||||
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defCluster = "127.0.0.1"
|
||||
defKeyspace = "mainflux"
|
||||
defDBUsername = ""
|
||||
defDBPassword = ""
|
||||
defDBPort = "9042"
|
||||
defThingsURL = "localhost:8181"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defJaegerURL = ""
|
||||
defThingsTimeout = "1" // in seconds
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defCluster = "127.0.0.1"
|
||||
defKeyspace = "mainflux"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBPort = "9042"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envLogLevel = "MF_CASSANDRA_READER_LOG_LEVEL"
|
||||
envPort = "MF_CASSANDRA_READER_PORT"
|
||||
envCluster = "MF_CASSANDRA_READER_DB_CLUSTER"
|
||||
envKeyspace = "MF_CASSANDRA_READER_DB_KEYSPACE"
|
||||
envDBUsername = "MF_CASSANDRA_READER_DB_USERNAME"
|
||||
envDBPassword = "MF_CASSANDRA_READER_DB_PASSWORD"
|
||||
envDBPort = "MF_CASSANDRA_READER_DB_PORT"
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
envClientTLS = "MF_CASSANDRA_READER_CLIENT_TLS"
|
||||
envCACerts = "MF_CASSANDRA_READER_CA_CERTS"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsTimeout = "MF_CASSANDRA_READER_THINGS_TIMEOUT"
|
||||
envLogLevel = "MF_CASSANDRA_READER_LOG_LEVEL"
|
||||
envPort = "MF_CASSANDRA_READER_PORT"
|
||||
envCluster = "MF_CASSANDRA_READER_DB_CLUSTER"
|
||||
envKeyspace = "MF_CASSANDRA_READER_DB_KEYSPACE"
|
||||
envDBUser = "MF_CASSANDRA_READER_DB_USER"
|
||||
envDBPass = "MF_CASSANDRA_READER_DB_PASS"
|
||||
envDBPort = "MF_CASSANDRA_READER_DB_PORT"
|
||||
envClientTLS = "MF_CASSANDRA_READER_CLIENT_TLS"
|
||||
envCACerts = "MF_CASSANDRA_READER_CA_CERTS"
|
||||
envServerCert = "MF_CASSANDRA_READER_SERVER_CERT"
|
||||
envServerKey = "MF_CASSANDRA_READER_SERVER_KEY"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsAuthURL = "MF_THINGS_AUTH_GRPC_URL"
|
||||
envThingsAuthTimeout = "MF_THINGS_AUTH_GRPC_TIMEOUT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
logLevel string
|
||||
port string
|
||||
dbCfg cassandra.DBConfig
|
||||
thingsURL string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
jaegerURL string
|
||||
thingsTimeout time.Duration
|
||||
logLevel string
|
||||
port string
|
||||
dbCfg cassandra.DBConfig
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
serverCert string
|
||||
serverKey string
|
||||
jaegerURL string
|
||||
thingsAuthURL string
|
||||
thingsAuthTimeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -89,12 +95,12 @@ func main() {
|
||||
thingsTracer, thingsCloser := initJaeger("things", cfg.jaegerURL, logger)
|
||||
defer thingsCloser.Close()
|
||||
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsTimeout)
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsAuthTimeout)
|
||||
repo := newService(session, logger)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(repo, tc, cfg.port, errs, logger)
|
||||
go startHTTPServer(repo, tc, cfg, errs, logger)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
@@ -115,8 +121,8 @@ func loadConfig() config {
|
||||
dbCfg := cassandra.DBConfig{
|
||||
Hosts: strings.Split(mainflux.Env(envCluster, defCluster), sep),
|
||||
Keyspace: mainflux.Env(envKeyspace, defKeyspace),
|
||||
Username: mainflux.Env(envDBUsername, defDBUsername),
|
||||
Password: mainflux.Env(envDBPassword, defDBPassword),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Port: dbPort,
|
||||
}
|
||||
|
||||
@@ -125,20 +131,22 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsTimeout, defThingsTimeout), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsTimeout, err.Error())
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
|
||||
return config{
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbCfg: dbCfg,
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsTimeout: time.Duration(timeout) * time.Second,
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbCfg: dbCfg,
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
serverCert: mainflux.Env(envServerCert, defServerCert),
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +176,7 @@ func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
conn, err := grpc.Dial(cfg.thingsAuthURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
@@ -222,8 +230,14 @@ func newService(session *gocql.Session, logger logger.Logger) readers.MessageRep
|
||||
return repo
|
||||
}
|
||||
|
||||
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, port string, errs chan error, logger logger.Logger) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("Cassandra reader service started, exposed port %s", port))
|
||||
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, cfg config, errs chan error, logger logger.Logger) {
|
||||
p := fmt.Sprintf(":%s", cfg.port)
|
||||
if cfg.serverCert != "" || cfg.serverKey != "" {
|
||||
logger.Info(fmt.Sprintf("Cassandra reader service started using https on port %s with cert %s key %s",
|
||||
cfg.port, cfg.serverCert, cfg.serverKey))
|
||||
errs <- http.ListenAndServeTLS(p, cfg.serverCert, cfg.serverKey, api.MakeHandler(repo, tc, "cassandra-reader"))
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Cassandra reader service started, exposed port %s", cfg.port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(repo, tc, "cassandra-reader"))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -14,16 +13,15 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/transformers/senml"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
"github.com/mainflux/mainflux/pkg/transformers/senml"
|
||||
"github.com/mainflux/mainflux/writers"
|
||||
"github.com/mainflux/mainflux/writers/api"
|
||||
"github.com/mainflux/mainflux/writers/cassandra"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@@ -31,33 +29,36 @@ const (
|
||||
svcName = "cassandra-writer"
|
||||
sep = ","
|
||||
|
||||
defNatsURL = nats.DefaultURL
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defCluster = "127.0.0.1"
|
||||
defKeyspace = "mainflux"
|
||||
defDBUsername = ""
|
||||
defDBPassword = ""
|
||||
defDBPort = "9042"
|
||||
defChanCfgPath = "/config/channels.toml"
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defCluster = "127.0.0.1"
|
||||
defKeyspace = "mainflux"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBPort = "9042"
|
||||
defSubjectsCfgPath = "/config/subjects.toml"
|
||||
defContentType = "application/senml+json"
|
||||
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_CASSANDRA_WRITER_LOG_LEVEL"
|
||||
envPort = "MF_CASSANDRA_WRITER_PORT"
|
||||
envCluster = "MF_CASSANDRA_WRITER_DB_CLUSTER"
|
||||
envKeyspace = "MF_CASSANDRA_WRITER_DB_KEYSPACE"
|
||||
envDBUsername = "MF_CASSANDRA_WRITER_DB_USERNAME"
|
||||
envDBPassword = "MF_CASSANDRA_WRITER_DB_PASSWORD"
|
||||
envDBPort = "MF_CASSANDRA_WRITER_DB_PORT"
|
||||
envChanCfgPath = "MF_CASSANDRA_WRITER_CHANNELS_CONFIG"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_CASSANDRA_WRITER_LOG_LEVEL"
|
||||
envPort = "MF_CASSANDRA_WRITER_PORT"
|
||||
envCluster = "MF_CASSANDRA_WRITER_DB_CLUSTER"
|
||||
envKeyspace = "MF_CASSANDRA_WRITER_DB_KEYSPACE"
|
||||
envDBUser = "MF_CASSANDRA_WRITER_DB_USER"
|
||||
envDBPass = "MF_CASSANDRA_WRITER_DB_PASS"
|
||||
envDBPort = "MF_CASSANDRA_WRITER_DB_PORT"
|
||||
envSubjectsCfgPath = "MF_CASSANDRA_WRITER_SUBJECTS_CONFIG"
|
||||
envContentType = "MF_CASSANDRA_WRITER_CONTENT_TYPE"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
dbCfg cassandra.DBConfig
|
||||
channels map[string]bool
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
subjectsCfgPath string
|
||||
contentType string
|
||||
dbCfg cassandra.DBConfig
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -68,15 +69,19 @@ func main() {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
nc := connectToNATS(cfg.natsURL, logger)
|
||||
defer nc.Close()
|
||||
pubSub, err := nats.NewPubSub(cfg.natsURL, "", logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pubSub.Close()
|
||||
|
||||
session := connectToCassandra(cfg.dbCfg, logger)
|
||||
defer session.Close()
|
||||
|
||||
repo := newService(session, logger)
|
||||
st := senml.New()
|
||||
if err := writers.Start(nc, repo, st, svcName, cfg.channels, logger); err != nil {
|
||||
st := senml.New(cfg.contentType)
|
||||
if err := writers.Start(pubSub, repo, st, svcName, cfg.subjectsCfgPath, logger); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create Cassandra writer: %s", err))
|
||||
}
|
||||
|
||||
@@ -103,58 +108,21 @@ func loadConfig() config {
|
||||
dbCfg := cassandra.DBConfig{
|
||||
Hosts: strings.Split(mainflux.Env(envCluster, defCluster), sep),
|
||||
Keyspace: mainflux.Env(envKeyspace, defKeyspace),
|
||||
Username: mainflux.Env(envDBUsername, defDBUsername),
|
||||
Password: mainflux.Env(envDBPassword, defDBPassword),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Port: dbPort,
|
||||
}
|
||||
|
||||
chanCfgPath := mainflux.Env(envChanCfgPath, defChanCfgPath)
|
||||
return config{
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbCfg: dbCfg,
|
||||
channels: loadChansConfig(chanCfgPath),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
subjectsCfgPath: mainflux.Env(envSubjectsCfgPath, defSubjectsCfgPath),
|
||||
contentType: mainflux.Env(envContentType, defContentType),
|
||||
dbCfg: dbCfg,
|
||||
}
|
||||
}
|
||||
|
||||
type channels struct {
|
||||
List []string `toml:"filter"`
|
||||
}
|
||||
|
||||
type chanConfig struct {
|
||||
Channels channels `toml:"channels"`
|
||||
}
|
||||
|
||||
func loadChansConfig(chanConfigPath string) map[string]bool {
|
||||
data, err := ioutil.ReadFile(chanConfigPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var chanCfg chanConfig
|
||||
if err := toml.Unmarshal(data, &chanCfg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
chans := map[string]bool{}
|
||||
for _, ch := range chanCfg.Channels.List {
|
||||
chans[ch] = true
|
||||
}
|
||||
|
||||
return chans
|
||||
}
|
||||
|
||||
func connectToNATS(url string, logger logger.Logger) *nats.Conn {
|
||||
nc, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
func connectToCassandra(dbCfg cassandra.DBConfig, logger logger.Logger) *gocql.Session {
|
||||
session, err := cassandra.Connect(dbCfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
authapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/certs"
|
||||
"github.com/mainflux/mainflux/certs/api"
|
||||
vault "github.com/mainflux/mainflux/certs/pki"
|
||||
"github.com/mainflux/mainflux/certs/postgres"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfsdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
)
|
||||
|
||||
const (
|
||||
defLogLevel = "error"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDB = "certs"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defPort = "8204"
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defBaseURL = "http://localhost"
|
||||
defThingsPrefix = ""
|
||||
defJaegerURL = ""
|
||||
defAuthnURL = "localhost:8181"
|
||||
defAuthnTimeout = "1s"
|
||||
|
||||
defSignCAPath = "ca.crt"
|
||||
defSignCAKeyPath = "ca.key"
|
||||
defSignHoursValid = "2048h"
|
||||
defSignRSABits = ""
|
||||
|
||||
defVaultHost = ""
|
||||
defVaultRole = "mainflux"
|
||||
defVaultToken = ""
|
||||
defVaultPKIPath = "pki_int"
|
||||
|
||||
envPort = "MF_CERTS_HTTP_PORT"
|
||||
envLogLevel = "MF_CERTS_LOG_LEVEL"
|
||||
envDBHost = "MF_CERTS_DB_HOST"
|
||||
envDBPort = "MF_CERTS_DB_PORT"
|
||||
envDBUser = "MF_CERTS_DB_USER"
|
||||
envDBPass = "MF_CERTS_DB_PASS"
|
||||
envDB = "MF_CERTS_DB"
|
||||
envDBSSLMode = "MF_CERTS_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_CERTS_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_CERTS_DB_SSL_KEY"
|
||||
envDBSSLRootCert = "MF_CERTS_DB_SSL_ROOT_CERT"
|
||||
envEncryptKey = "MF_CERTS_ENCRYPT_KEY"
|
||||
envClientTLS = "MF_CERTS_CLIENT_TLS"
|
||||
envCACerts = "MF_CERTS_CA_CERTS"
|
||||
envServerCert = "MF_CERTS_SERVER_CERT"
|
||||
envServerKey = "MF_CERTS_SERVER_KEY"
|
||||
envBaseURL = "MF_SDK_BASE_URL"
|
||||
envThingsPrefix = "MF_SDK_THINGS_PREFIX"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envAuthnURL = "MF_AUTHN_GRPC_URL"
|
||||
envAuthnTimeout = "MF_AUTHN_GRPC_TIMEOUT"
|
||||
|
||||
envSignCAPath = "MF_CERTS_SIGN_CA_PATH"
|
||||
envSignCAKey = "MF_CERTS_SIGN_CA_KEY_PATH"
|
||||
envSignHoursValid = "MF_CERTS_SIGN_HOURS_VALID"
|
||||
envSignRSABits = "MF_CERTS_SIGN_RSA_BITS"
|
||||
|
||||
envVaultHost = "MF_CERTS_VAULT_HOST"
|
||||
envVaultPKIPath = "MF_CERTS_VAULT_PKI_PATH"
|
||||
envVaultRole = "MF_CERTS_VAULT_ROLE"
|
||||
envVaultToken = "MF_CERTS_VAULT_TOKEN"
|
||||
)
|
||||
|
||||
var (
|
||||
errFailedCertLoading = errors.New("failed to load certificate")
|
||||
errFailedCertDecode = errors.New("failed to decode certificate")
|
||||
errMissingCACertificate = errors.New("missing CA")
|
||||
errPrivateKeyEmpty = errors.New("private key empty")
|
||||
errPrivateKeyUnsupportedType = errors.New("private key unsupported type")
|
||||
errCertsRemove = errors.New("failed to remove certificate")
|
||||
errCACertificateDoesntExist = errors.New("CA certificate doesnt exist")
|
||||
errCAKeyDoesntExist = errors.New("CA certificate key doesnt exist")
|
||||
)
|
||||
|
||||
type config struct {
|
||||
logLevel string
|
||||
dbConfig postgres.Config
|
||||
clientTLS bool
|
||||
encKey []byte
|
||||
caCerts string
|
||||
httpPort string
|
||||
serverCert string
|
||||
serverKey string
|
||||
baseURL string
|
||||
thingsPrefix string
|
||||
jaegerURL string
|
||||
authnURL string
|
||||
authnTimeout time.Duration
|
||||
// Sign and issue certificates
|
||||
// without 3rd party PKI
|
||||
signCAPath string
|
||||
signCAKeyPath string
|
||||
signRSABits int
|
||||
signHoursValid string
|
||||
// 3rd party PKI API access settings
|
||||
pkiPath string
|
||||
pkiToken string
|
||||
pkiHost string
|
||||
pkiRole string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, err := mflog.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
tlsCert, caCert, err := loadCertificates(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to load CA certificates for issuing client certs")
|
||||
}
|
||||
|
||||
pkiClient, err := vault.NewVaultClient(cfg.pkiToken, cfg.pkiHost, cfg.pkiPath, cfg.pkiRole)
|
||||
if err != nil {
|
||||
logger.Error("Failed to init vault client")
|
||||
}
|
||||
|
||||
db := connectToDB(cfg.dbConfig, logger)
|
||||
defer db.Close()
|
||||
|
||||
authTracer, authCloser := initJaeger("auth", cfg.jaegerURL, logger)
|
||||
defer authCloser.Close()
|
||||
|
||||
authConn := connectToAuth(cfg, logger)
|
||||
defer authConn.Close()
|
||||
|
||||
auth := authapi.NewClient(authTracer, authConn, cfg.authnTimeout)
|
||||
|
||||
svc := newService(auth, db, logger, nil, tlsCert, caCert, cfg, pkiClient)
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Certs 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(envDB, defDB),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
|
||||
}
|
||||
|
||||
authnTimeout, err := time.ParseDuration(mainflux.Env(envAuthnTimeout, defAuthnTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envAuthnTimeout, err.Error())
|
||||
}
|
||||
|
||||
signRSABits, err := strconv.Atoi(mainflux.Env(envSignRSABits, defSignRSABits))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envSignRSABits, err.Error())
|
||||
}
|
||||
|
||||
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),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: authnTimeout,
|
||||
|
||||
signCAKeyPath: mainflux.Env(envSignCAKey, defSignCAKeyPath),
|
||||
signCAPath: mainflux.Env(envSignCAPath, defSignCAPath),
|
||||
signHoursValid: mainflux.Env(envSignHoursValid, defSignHoursValid),
|
||||
signRSABits: signRSABits,
|
||||
|
||||
pkiToken: mainflux.Env(envVaultToken, defVaultToken),
|
||||
pkiPath: mainflux.Env(envVaultPKIPath, defVaultPKIPath),
|
||||
pkiRole: mainflux.Env(envVaultRole, defVaultRole),
|
||||
pkiHost: mainflux.Env(envVaultHost, defVaultHost),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func connectToRedis(redisURL, redisPass, redisDB string, logger mflog.Logger) *redis.Client {
|
||||
db, err := strconv.Atoi(redisDB)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to redis: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return redis.NewClient(&redis.Options{
|
||||
Addr: redisURL,
|
||||
Password: redisPass,
|
||||
DB: db,
|
||||
})
|
||||
}
|
||||
|
||||
func connectToDB(dbConfig postgres.Config, logger logger.Logger) *sqlx.DB {
|
||||
db, err := postgres.Connect(dbConfig)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func connectToAuth(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.authnURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to authn service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func initJaeger(svcName, url string, logger logger.Logger) (opentracing.Tracer, io.Closer) {
|
||||
if url == "" {
|
||||
return opentracing.NoopTracer{}, ioutil.NopCloser(nil)
|
||||
}
|
||||
|
||||
tracer, closer, err := jconfig.Configuration{
|
||||
ServiceName: svcName,
|
||||
Sampler: &jconfig.SamplerConfig{
|
||||
Type: "const",
|
||||
Param: 1,
|
||||
},
|
||||
Reporter: &jconfig.ReporterConfig{
|
||||
LocalAgentHostPort: url,
|
||||
LogSpans: true,
|
||||
},
|
||||
}.NewTracer()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to init Jaeger client: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return tracer, closer
|
||||
}
|
||||
|
||||
func newService(auth mainflux.AuthNServiceClient, db *sqlx.DB, logger mflog.Logger, esClient *redis.Client, tlsCert tls.Certificate, x509Cert *x509.Certificate, cfg config, pkiAgent vault.Agent) certs.Service {
|
||||
certsRepo := postgres.NewRepository(db, logger)
|
||||
|
||||
certsConfig := certs.Config{
|
||||
LogLevel: cfg.logLevel,
|
||||
ClientTLS: cfg.clientTLS,
|
||||
CaCerts: cfg.caCerts,
|
||||
HTTPPort: cfg.httpPort,
|
||||
ServerCert: cfg.serverCert,
|
||||
ServerKey: cfg.serverKey,
|
||||
BaseURL: cfg.baseURL,
|
||||
ThingsPrefix: cfg.thingsPrefix,
|
||||
JaegerURL: cfg.jaegerURL,
|
||||
AuthnURL: cfg.authnURL,
|
||||
AuthnTimeout: cfg.authnTimeout,
|
||||
SignTLSCert: tlsCert,
|
||||
SignX509Cert: x509Cert,
|
||||
SignHoursValid: cfg.signHoursValid,
|
||||
SignRSABits: cfg.signRSABits,
|
||||
PKIToken: cfg.pkiToken,
|
||||
PKIHost: cfg.pkiHost,
|
||||
PKIPath: cfg.pkiPath,
|
||||
PKIRole: cfg.pkiRole,
|
||||
}
|
||||
|
||||
config := mfsdk.Config{
|
||||
BaseURL: cfg.baseURL,
|
||||
ThingsPrefix: cfg.thingsPrefix,
|
||||
}
|
||||
|
||||
sdk := mfsdk.NewSDK(config)
|
||||
|
||||
svc := certs.New(auth, certsRepo, sdk, certsConfig, pkiAgent)
|
||||
svc = api.NewLoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "certs",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "certs",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
return svc
|
||||
}
|
||||
|
||||
func startHTTPServer(svc certs.Service, cfg config, logger mflog.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.httpPort)
|
||||
if cfg.serverCert != "" || cfg.serverKey != "" {
|
||||
logger.Info(fmt.Sprintf("Certs 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))
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Certs service started using http on port %s", cfg.httpPort))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
|
||||
}
|
||||
|
||||
func loadCertificates(conf config) (tls.Certificate, *x509.Certificate, error) {
|
||||
var tlsCert tls.Certificate
|
||||
var caCert *x509.Certificate
|
||||
|
||||
if conf.signCAPath == "" || conf.signCAKeyPath == "" {
|
||||
return tlsCert, caCert, nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(conf.signCAPath); os.IsNotExist(err) {
|
||||
return tlsCert, caCert, errCACertificateDoesntExist
|
||||
}
|
||||
|
||||
if _, err := os.Stat(conf.signCAKeyPath); os.IsNotExist(err) {
|
||||
return tlsCert, caCert, errCAKeyDoesntExist
|
||||
}
|
||||
|
||||
tlsCert, err := tls.LoadX509KeyPair(conf.signCAPath, conf.signCAKeyPath)
|
||||
if err != nil {
|
||||
return tlsCert, caCert, errors.Wrap(errFailedCertLoading, err)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(conf.signCAPath)
|
||||
if err != nil {
|
||||
return tlsCert, caCert, errors.Wrap(errFailedCertLoading, err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(b)
|
||||
if block == nil {
|
||||
log.Fatalf("No PEM data found, failed to decode CA")
|
||||
}
|
||||
|
||||
caCert, err = x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return tlsCert, caCert, errors.Wrap(errFailedCertDecode, err)
|
||||
}
|
||||
|
||||
return tlsCert, caCert, nil
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
+19
-1
@@ -7,7 +7,7 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/mainflux/mainflux/cli"
|
||||
"github.com/mainflux/mainflux/sdk/go"
|
||||
sdk "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -16,10 +16,14 @@ func main() {
|
||||
sdkConf := sdk.Config{
|
||||
BaseURL: "http://localhost",
|
||||
ReaderURL: "http://localhost:8905",
|
||||
BootstrapURL: "http://localhost:8202",
|
||||
CertsURL: "http://localhost:8204",
|
||||
ReaderPrefix: "",
|
||||
UsersPrefix: "",
|
||||
GroupsPrefix: "",
|
||||
ThingsPrefix: "",
|
||||
HTTPAdapterPrefix: "http",
|
||||
BootstrapPrefix: "things",
|
||||
MsgContentType: sdk.ContentType(msgContentType),
|
||||
TLSVerification: false,
|
||||
}
|
||||
@@ -38,17 +42,23 @@ func main() {
|
||||
versionCmd := cli.NewVersionCmd()
|
||||
usersCmd := cli.NewUsersCmd()
|
||||
thingsCmd := cli.NewThingsCmd()
|
||||
groupsCmd := cli.NewGroupsCmd()
|
||||
channelsCmd := cli.NewChannelsCmd()
|
||||
messagesCmd := cli.NewMessagesCmd()
|
||||
provisionCmd := cli.NewProvisionCmd()
|
||||
bootstrapCmd := cli.NewBootstrapCmd()
|
||||
certsCmd := cli.NewCertsCmd()
|
||||
|
||||
// Root Commands
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.AddCommand(usersCmd)
|
||||
rootCmd.AddCommand(groupsCmd)
|
||||
rootCmd.AddCommand(thingsCmd)
|
||||
rootCmd.AddCommand(channelsCmd)
|
||||
rootCmd.AddCommand(messagesCmd)
|
||||
rootCmd.AddCommand(provisionCmd)
|
||||
rootCmd.AddCommand(bootstrapCmd)
|
||||
rootCmd.AddCommand(certsCmd)
|
||||
|
||||
// Root Flags
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
@@ -75,6 +85,14 @@ func main() {
|
||||
"Mainflux things service prefix",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&sdkConf.GroupsPrefix,
|
||||
"groups-prefix",
|
||||
"g",
|
||||
sdkConf.GroupsPrefix,
|
||||
"Mainflux groups service prefix",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&sdkConf.HTTPAdapterPrefix,
|
||||
"http-prefix",
|
||||
|
||||
+50
-66
@@ -15,55 +15,50 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
gocoap "github.com/dustin/go-coap"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/coap"
|
||||
"github.com/mainflux/mainflux/coap/api"
|
||||
"github.com/mainflux/mainflux/coap/nats"
|
||||
logger "github.com/mainflux/mainflux/logger"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/auth/grpc"
|
||||
broker "github.com/nats-io/nats.go"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
gocoap "github.com/plgd-dev/go-coap/v2"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
defPort = "5683"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defThingsURL = "localhost:8181"
|
||||
defLogLevel = "error"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defPingPeriod = "12"
|
||||
defJaegerURL = ""
|
||||
defThingsTimeout = "1" // in seconds
|
||||
defPort = "5683"
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defLogLevel = "error"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envPort = "MF_COAP_ADAPTER_PORT"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
envLogLevel = "MF_COAP_ADAPTER_LOG_LEVEL"
|
||||
envClientTLS = "MF_COAP_ADAPTER_CLIENT_TLS"
|
||||
envCACerts = "MF_COAP_ADAPTER_CA_CERTS"
|
||||
envPingPeriod = "MF_COAP_ADAPTER_PING_PERIOD"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsTimeout = "MF_COAP_ADAPTER_THINGS_TIMEOUT"
|
||||
envPort = "MF_COAP_ADAPTER_PORT"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_COAP_ADAPTER_LOG_LEVEL"
|
||||
envClientTLS = "MF_COAP_ADAPTER_CLIENT_TLS"
|
||||
envCACerts = "MF_COAP_ADAPTER_CA_CERTS"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsAuthURL = "MF_THINGS_AUTH_GRPC_URL"
|
||||
envThingsAuthTimeout = "MF_THINGS_AUTH_GRPC_TIMEOUT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
port string
|
||||
natsURL string
|
||||
thingsURL string
|
||||
logLevel string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
pingPeriod time.Duration
|
||||
jaegerURL string
|
||||
thingsTimeout time.Duration
|
||||
port string
|
||||
natsURL string
|
||||
logLevel string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
jaegerURL string
|
||||
thingsAuthURL string
|
||||
thingsAuthTimeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -74,23 +69,22 @@ func main() {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
nc, err := broker.Connect(cfg.natsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn := connectToThings(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
thingsTracer, thingsCloser := initJaeger("things", cfg.jaegerURL, logger)
|
||||
defer thingsCloser.Close()
|
||||
|
||||
cc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsTimeout)
|
||||
respChan := make(chan string, 10000)
|
||||
pubsub := nats.New(nc)
|
||||
svc := coap.New(pubsub, cc, respChan)
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsAuthTimeout)
|
||||
|
||||
nc, err := broker.Connect(cfg.natsURL)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
svc := coap.New(tc, nc)
|
||||
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
|
||||
svc = api.MetricsMiddleware(
|
||||
@@ -112,7 +106,7 @@ func main() {
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(cfg.port, logger, errs)
|
||||
go startCOAPServer(cfg, svc, cc, respChan, logger, errs)
|
||||
go startCOAPServer(cfg, svc, nil, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
@@ -130,30 +124,20 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
pp, err := strconv.ParseInt(mainflux.Env(envPingPeriod, defPingPeriod), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envPingPeriod)
|
||||
}
|
||||
|
||||
if pp < 1 || pp > 24 {
|
||||
log.Fatalf("Value of %s must be between 1 and 24", envPingPeriod)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsTimeout, defThingsTimeout), 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsTimeout, err.Error())
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
|
||||
return config{
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
pingPeriod: time.Duration(pp),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsTimeout: time.Duration(timeout) * time.Second,
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +157,7 @@ func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
conn, err := grpc.Dial(cfg.thingsAuthURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
@@ -211,8 +195,8 @@ func startHTTPServer(port string, logger logger.Logger, errs chan error) {
|
||||
errs <- http.ListenAndServe(p, api.MakeHTTPHandler())
|
||||
}
|
||||
|
||||
func startCOAPServer(cfg config, svc coap.Service, auth mainflux.ThingsServiceClient, respChan chan<- string, l logger.Logger, errs chan error) {
|
||||
func startCOAPServer(cfg config, svc coap.Service, auth mainflux.ThingsServiceClient, l logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.port)
|
||||
l.Info(fmt.Sprintf("CoAP adapter service started, exposed port %s", cfg.port))
|
||||
errs <- gocoap.ListenAndServe("udp", p, api.MakeCOAPHandler(svc, auth, l, respChan, cfg.pingPeriod))
|
||||
errs <- gocoap.ListenAndServe("udp", p, api.MakeCoAPHandler(svc, l))
|
||||
}
|
||||
|
||||
+46
-49
@@ -21,49 +21,47 @@ import (
|
||||
"github.com/mainflux/mainflux"
|
||||
adapter "github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/http/api"
|
||||
"github.com/mainflux/mainflux/http/nats"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/auth/grpc"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defPort = "8180"
|
||||
defLogLevel = "error"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defThingsURL = "localhost:8181"
|
||||
defJaegerURL = ""
|
||||
defThingsTimeout = "1" // in seconds
|
||||
defLogLevel = "error"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defPort = "8180"
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envClientTLS = "MF_HTTP_ADAPTER_CLIENT_TLS"
|
||||
envCACerts = "MF_HTTP_ADAPTER_CA_CERTS"
|
||||
envPort = "MF_HTTP_ADAPTER_PORT"
|
||||
envLogLevel = "MF_HTTP_ADAPTER_LOG_LEVEL"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsTimeout = "MF_HTTP_ADAPTER_THINGS_TIMEOUT"
|
||||
envLogLevel = "MF_HTTP_ADAPTER_LOG_LEVEL"
|
||||
envClientTLS = "MF_HTTP_ADAPTER_CLIENT_TLS"
|
||||
envCACerts = "MF_HTTP_ADAPTER_CA_CERTS"
|
||||
envPort = "MF_HTTP_ADAPTER_PORT"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsAuthURL = "MF_THINGS_AUTH_GRPC_URL"
|
||||
envThingsAuthTimeout = "MF_THINGS_AUTH_GRPC_TIMEOUT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
thingsURL string
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
jaegerURL string
|
||||
thingsTimeout time.Duration
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
jaegerURL string
|
||||
thingsAuthURL string
|
||||
thingsAuthTimeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
@@ -71,13 +69,6 @@ func main() {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
nc, err := broker.Connect(cfg.natsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn := connectToThings(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
@@ -87,10 +78,16 @@ func main() {
|
||||
thingsTracer, thingsCloser := initJaeger("things", cfg.jaegerURL, logger)
|
||||
defer thingsCloser.Close()
|
||||
|
||||
cc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsTimeout)
|
||||
pub := nats.NewMessagePublisher(nc)
|
||||
pub, err := nats.NewPublisher(cfg.natsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pub.Close()
|
||||
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsAuthTimeout)
|
||||
svc := adapter.New(pub, tc)
|
||||
|
||||
svc := adapter.New(pub, cc)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
@@ -132,20 +129,20 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsTimeout, defThingsTimeout), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsTimeout, err.Error())
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
|
||||
return config{
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsTimeout: time.Duration(timeout) * time.Second,
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +186,7 @@ func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
conn, err := grpc.Dial(cfg.thingsAuthURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
|
||||
+70
-56
@@ -28,46 +28,52 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defThingsURL = "localhost:8181"
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDBName = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "8086"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defJaegerURL = ""
|
||||
defThingsTimeout = "1" // in seconds
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDB = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "8086"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
envLogLevel = "MF_INFLUX_READER_LOG_LEVEL"
|
||||
envPort = "MF_INFLUX_READER_PORT"
|
||||
envDBName = "MF_INFLUX_READER_DB_NAME"
|
||||
envDBHost = "MF_INFLUX_READER_DB_HOST"
|
||||
envDBPort = "MF_INFLUX_READER_DB_PORT"
|
||||
envDBUser = "MF_INFLUX_READER_DB_USER"
|
||||
envDBPass = "MF_INFLUX_READER_DB_PASS"
|
||||
envClientTLS = "MF_INFLUX_READER_CLIENT_TLS"
|
||||
envCACerts = "MF_INFLUX_READER_CA_CERTS"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsTimeout = "MF_INFLUX_READER_THINGS_TIMEOUT"
|
||||
envLogLevel = "MF_INFLUX_READER_LOG_LEVEL"
|
||||
envPort = "MF_INFLUX_READER_PORT"
|
||||
envDB = "MF_INFLUX_READER_DB"
|
||||
envDBHost = "MF_INFLUX_READER_DB_HOST"
|
||||
envDBPort = "MF_INFLUX_READER_DB_PORT"
|
||||
envDBUser = "MF_INFLUX_READER_DB_USER"
|
||||
envDBPass = "MF_INFLUX_READER_DB_PASS"
|
||||
envClientTLS = "MF_INFLUX_READER_CLIENT_TLS"
|
||||
envCACerts = "MF_INFLUX_READER_CA_CERTS"
|
||||
envServerCert = "MF_INFLUX_READER_SERVER_CERT"
|
||||
envServerKey = "MF_INFLUX_READER_SERVER_KEY"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsAuthURL = "MF_THINGS_AUTH_GRPC_URL"
|
||||
envThingsAuthTimeout = "MF_THINGS_AUTH_GRPC_TIMEOUT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
thingsURL string
|
||||
logLevel string
|
||||
port string
|
||||
dbName string
|
||||
dbHost string
|
||||
dbPort string
|
||||
dbUser string
|
||||
dbPass string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
jaegerURL string
|
||||
thingsTimeout time.Duration
|
||||
logLevel string
|
||||
port string
|
||||
dbName string
|
||||
dbHost string
|
||||
dbPort string
|
||||
dbUser string
|
||||
dbPass string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
serverCert string
|
||||
serverKey string
|
||||
jaegerURL string
|
||||
thingsAuthURL string
|
||||
thingsAuthTimeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -82,7 +88,7 @@ func main() {
|
||||
thingsTracer, thingsCloser := initJaeger("things", cfg.jaegerURL, logger)
|
||||
defer thingsCloser.Close()
|
||||
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsTimeout)
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsAuthTimeout)
|
||||
|
||||
client, err := influxdata.NewHTTPClient(clientCfg)
|
||||
if err != nil {
|
||||
@@ -100,7 +106,7 @@ func main() {
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
go startHTTPServer(repo, tc, cfg.port, logger, errs)
|
||||
go startHTTPServer(repo, tc, cfg, logger, errs)
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("InfluxDB writer service terminated: %s", err))
|
||||
@@ -112,24 +118,26 @@ func loadConfigs() (config, influxdata.HTTPConfig) {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsTimeout, defThingsTimeout), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsTimeout, err.Error())
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
|
||||
cfg := config{
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbName: mainflux.Env(envDBName, defDBName),
|
||||
dbHost: mainflux.Env(envDBHost, defDBHost),
|
||||
dbPort: mainflux.Env(envDBPort, defDBPort),
|
||||
dbUser: mainflux.Env(envDBUser, defDBUser),
|
||||
dbPass: mainflux.Env(envDBPass, defDBPass),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsTimeout: time.Duration(timeout) * time.Second,
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbName: mainflux.Env(envDB, defDB),
|
||||
dbHost: mainflux.Env(envDBHost, defDBHost),
|
||||
dbPort: mainflux.Env(envDBPort, defDBPort),
|
||||
dbUser: mainflux.Env(envDBUser, defDBUser),
|
||||
dbPass: mainflux.Env(envDBPass, defDBPass),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
serverCert: mainflux.Env(envServerCert, defServerCert),
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
|
||||
clientCfg := influxdata.HTTPConfig{
|
||||
@@ -157,7 +165,7 @@ func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
conn, err := grpc.Dial(cfg.thingsAuthURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
@@ -211,8 +219,14 @@ func newService(client influxdata.Client, dbName string, logger logger.Logger) r
|
||||
return repo
|
||||
}
|
||||
|
||||
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, port string, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("InfluxDB reader service started, exposed port %s", port))
|
||||
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, cfg config, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.port)
|
||||
if cfg.serverCert != "" || cfg.serverKey != "" {
|
||||
logger.Info(fmt.Sprintf("InfluxDB reader service started using https on port %s with cert %s key %s",
|
||||
cfg.port, cfg.serverCert, cfg.serverKey))
|
||||
errs <- http.ListenAndServeTLS(p, cfg.serverCert, cfg.serverKey, api.MakeHandler(repo, tc, "influxdb-reader"))
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("InfluxDB reader service started, exposed port %s", cfg.port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(repo, tc, "influxdb-reader"))
|
||||
}
|
||||
|
||||
+47
-72
@@ -5,60 +5,61 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
influxdata "github.com/influxdata/influxdb/client/v2"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/transformers/senml"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
"github.com/mainflux/mainflux/pkg/transformers/senml"
|
||||
"github.com/mainflux/mainflux/writers"
|
||||
"github.com/mainflux/mainflux/writers/api"
|
||||
"github.com/mainflux/mainflux/writers/influxdb"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
svcName = "influxdb-writer"
|
||||
|
||||
defNatsURL = nats.DefaultURL
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDBName = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "8086"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defChanCfgPath = "/config/channels.toml"
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDB = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "8086"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defSubjectsCfgPath = "/config/subjects.toml"
|
||||
defContentType = "application/senml+json"
|
||||
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_INFLUX_WRITER_LOG_LEVEL"
|
||||
envPort = "MF_INFLUX_WRITER_PORT"
|
||||
envDBName = "MF_INFLUX_WRITER_DB_NAME"
|
||||
envDBHost = "MF_INFLUX_WRITER_DB_HOST"
|
||||
envDBPort = "MF_INFLUX_WRITER_DB_PORT"
|
||||
envDBUser = "MF_INFLUX_WRITER_DB_USER"
|
||||
envDBPass = "MF_INFLUX_WRITER_DB_PASS"
|
||||
envChanCfgPath = "MF_INFLUX_WRITER_CHANNELS_CONFIG"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_INFLUX_WRITER_LOG_LEVEL"
|
||||
envPort = "MF_INFLUX_WRITER_PORT"
|
||||
envDB = "MF_INFLUX_WRITER_DB"
|
||||
envDBHost = "MF_INFLUX_WRITER_DB_HOST"
|
||||
envDBPort = "MF_INFLUX_WRITER_DB_PORT"
|
||||
envDBUser = "MF_INFLUX_WRITER_DB_USER"
|
||||
envDBPass = "MF_INFLUX_WRITER_DB_PASS"
|
||||
envSubjectsCfgPath = "MF_INFLUX_WRITER_SUBJECTS_CONFIG"
|
||||
envContentType = "MF_INFLUX_WRITER_CONTENT_TYPE"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
dbName string
|
||||
dbHost string
|
||||
dbPort string
|
||||
dbUser string
|
||||
dbPass string
|
||||
channels map[string]bool
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
dbName string
|
||||
dbHost string
|
||||
dbPort string
|
||||
dbUser string
|
||||
dbPass string
|
||||
subjectsCfgPath string
|
||||
contentType string
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -69,12 +70,12 @@ func main() {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
nc, err := nats.Connect(cfg.natsURL)
|
||||
pubSub, err := nats.NewPubSub(cfg.natsURL, "", logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
defer pubSub.Close()
|
||||
|
||||
client, err := influxdata.NewHTTPClient(clientCfg)
|
||||
if err != nil {
|
||||
@@ -88,8 +89,9 @@ func main() {
|
||||
counter, latency := makeMetrics()
|
||||
repo = api.LoggingMiddleware(repo, logger)
|
||||
repo = api.MetricsMiddleware(repo, counter, latency)
|
||||
st := senml.New()
|
||||
if err := writers.Start(nc, repo, st, svcName, cfg.channels, logger); err != nil {
|
||||
st := senml.New(cfg.contentType)
|
||||
|
||||
if err := writers.Start(pubSub, repo, st, svcName, cfg.subjectsCfgPath, logger); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to start InfluxDB writer: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -108,17 +110,17 @@ func main() {
|
||||
}
|
||||
|
||||
func loadConfigs() (config, influxdata.HTTPConfig) {
|
||||
chanCfgPath := mainflux.Env(envChanCfgPath, defChanCfgPath)
|
||||
cfg := config{
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbName: mainflux.Env(envDBName, defDBName),
|
||||
dbHost: mainflux.Env(envDBHost, defDBHost),
|
||||
dbPort: mainflux.Env(envDBPort, defDBPort),
|
||||
dbUser: mainflux.Env(envDBUser, defDBUser),
|
||||
dbPass: mainflux.Env(envDBPass, defDBPass),
|
||||
channels: loadChansConfig(chanCfgPath),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbName: mainflux.Env(envDB, defDB),
|
||||
dbHost: mainflux.Env(envDBHost, defDBHost),
|
||||
dbPort: mainflux.Env(envDBPort, defDBPort),
|
||||
dbUser: mainflux.Env(envDBUser, defDBUser),
|
||||
dbPass: mainflux.Env(envDBPass, defDBPass),
|
||||
subjectsCfgPath: mainflux.Env(envSubjectsCfgPath, defSubjectsCfgPath),
|
||||
contentType: mainflux.Env(envContentType, defContentType),
|
||||
}
|
||||
|
||||
clientCfg := influxdata.HTTPConfig{
|
||||
@@ -130,33 +132,6 @@ func loadConfigs() (config, influxdata.HTTPConfig) {
|
||||
return cfg, clientCfg
|
||||
}
|
||||
|
||||
type channels struct {
|
||||
List []string `toml:"filter"`
|
||||
}
|
||||
|
||||
type chanConfig struct {
|
||||
Channels channels `toml:"channels"`
|
||||
}
|
||||
|
||||
func loadChansConfig(chanConfigPath string) map[string]bool {
|
||||
data, err := ioutil.ReadFile(chanConfigPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var chanCfg chanConfig
|
||||
if err := toml.Unmarshal(data, &chanCfg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
chans := map[string]bool{}
|
||||
for _, ch := range chanCfg.Channels.List {
|
||||
chans[ch] = true
|
||||
}
|
||||
|
||||
return chans
|
||||
}
|
||||
|
||||
func makeMetrics() (*kitprometheus.Counter, *kitprometheus.Summary) {
|
||||
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "influxdb",
|
||||
|
||||
+10
-20
@@ -19,19 +19,18 @@ import (
|
||||
"github.com/mainflux/mainflux/lora"
|
||||
"github.com/mainflux/mainflux/lora/api"
|
||||
"github.com/mainflux/mainflux/lora/mqtt"
|
||||
pub "github.com/mainflux/mainflux/lora/nats"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux/lora/redis"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
defLogLevel = "error"
|
||||
defHTTPPort = "8180"
|
||||
defLoraMsgURL = "tcp://localhost:1883"
|
||||
defNatsURL = nats.DefaultURL
|
||||
defLogLevel = "error"
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defESURL = "localhost:6379"
|
||||
defESPass = ""
|
||||
defESDB = "0"
|
||||
@@ -80,23 +79,25 @@ func main() {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
natsConn := connectToNATS(cfg.natsURL, logger)
|
||||
defer natsConn.Close()
|
||||
|
||||
rmConn := connectToRedis(cfg.routeMapURL, cfg.routeMapPass, cfg.routeMapDB, logger)
|
||||
defer rmConn.Close()
|
||||
|
||||
esConn := connectToRedis(cfg.esURL, cfg.esPass, cfg.esDB, logger)
|
||||
defer esConn.Close()
|
||||
|
||||
publisher := pub.NewMessagePublisher(natsConn)
|
||||
pub, err := nats.NewPublisher(cfg.natsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pub.Close()
|
||||
|
||||
thingRM := newRouteMapRepositoy(rmConn, thingsRMPrefix, logger)
|
||||
chanRM := newRouteMapRepositoy(rmConn, channelsRMPrefix, logger)
|
||||
|
||||
mqttConn := connectToMQTTBroker(cfg.loraMsgURL, logger)
|
||||
|
||||
svc := lora.New(publisher, thingRM, chanRM)
|
||||
svc := lora.New(pub, thingRM, chanRM)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
@@ -147,17 +148,6 @@ func loadConfig() config {
|
||||
}
|
||||
}
|
||||
|
||||
func connectToNATS(url string, logger logger.Logger) *nats.Conn {
|
||||
conn, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger.Info("Connected to NATS")
|
||||
return conn
|
||||
}
|
||||
|
||||
func connectToMQTTBroker(loraURL string, logger logger.Logger) mqttPaho.Client {
|
||||
opts := mqttPaho.NewClientOptions()
|
||||
opts.AddBroker(loraURL)
|
||||
|
||||
+62
-48
@@ -33,40 +33,46 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defThingsURL = "localhost:8181"
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDBName = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "27017"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defJaegerURL = ""
|
||||
defThingsTimeout = "1" // in seconds
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDB = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "27017"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
envLogLevel = "MF_MONGO_READER_LOG_LEVEL"
|
||||
envPort = "MF_MONGO_READER_PORT"
|
||||
envDBName = "MF_MONGO_READER_DB_NAME"
|
||||
envDBHost = "MF_MONGO_READER_DB_HOST"
|
||||
envDBPort = "MF_MONGO_READER_DB_PORT"
|
||||
envClientTLS = "MF_MONGO_READER_CLIENT_TLS"
|
||||
envCACerts = "MF_MONGO_READER_CA_CERTS"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsTimeout = "MF_MONGO_READER_THINGS_TIMEOUT"
|
||||
envLogLevel = "MF_MONGO_READER_LOG_LEVEL"
|
||||
envPort = "MF_MONGO_READER_PORT"
|
||||
envDB = "MF_MONGO_READER_DB"
|
||||
envDBHost = "MF_MONGO_READER_DB_HOST"
|
||||
envDBPort = "MF_MONGO_READER_DB_PORT"
|
||||
envClientTLS = "MF_MONGO_READER_CLIENT_TLS"
|
||||
envCACerts = "MF_MONGO_READER_CA_CERTS"
|
||||
envServerCert = "MF_MONGO_READER_SERVER_CERT"
|
||||
envServerKey = "MF_MONGO_READER_SERVER_KEY"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsAuthURL = "MF_THINGS_AUTH_GRPC_URL"
|
||||
envThingsAuthTimeout = "MF_THINGS_AUTH_GRPC_TIMEOUT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
thingsURL string
|
||||
logLevel string
|
||||
port string
|
||||
dbName string
|
||||
dbHost string
|
||||
dbPort string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
jaegerURL string
|
||||
thingsTimeout time.Duration
|
||||
logLevel string
|
||||
port string
|
||||
dbName string
|
||||
dbHost string
|
||||
dbPort string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
serverCert string
|
||||
serverKey string
|
||||
jaegerURL string
|
||||
thingsAuthURL string
|
||||
thingsAuthTimeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -82,7 +88,7 @@ func main() {
|
||||
thingsTracer, thingsCloser := initJaeger("things", cfg.jaegerURL, logger)
|
||||
defer thingsCloser.Close()
|
||||
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsTimeout)
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsAuthTimeout)
|
||||
|
||||
db := connectToMongoDB(cfg.dbHost, cfg.dbPort, cfg.dbName, logger)
|
||||
|
||||
@@ -95,7 +101,7 @@ func main() {
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
go startHTTPServer(repo, tc, cfg.port, logger, errs)
|
||||
go startHTTPServer(repo, tc, cfg, logger, errs)
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("MongoDB reader service terminated: %s", err))
|
||||
@@ -107,22 +113,24 @@ func loadConfigs() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsTimeout, defThingsTimeout), 10, 64)
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsTimeout, err.Error())
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
|
||||
return config{
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbName: mainflux.Env(envDBName, defDBName),
|
||||
dbHost: mainflux.Env(envDBHost, defDBHost),
|
||||
dbPort: mainflux.Env(envDBPort, defDBPort),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsTimeout: time.Duration(timeout) * time.Second,
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbName: mainflux.Env(envDB, defDB),
|
||||
dbHost: mainflux.Env(envDBHost, defDBHost),
|
||||
dbPort: mainflux.Env(envDBPort, defDBPort),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
serverCert: mainflux.Env(envServerCert, defServerCert),
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +185,7 @@ func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
conn, err := grpc.Dial(cfg.thingsAuthURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
@@ -207,8 +215,14 @@ func newService(db *mongo.Database, logger logger.Logger) readers.MessageReposit
|
||||
return repo
|
||||
}
|
||||
|
||||
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, port string, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
logger.Info(fmt.Sprintf("Mongo reader service started, exposed port %s", port))
|
||||
func startHTTPServer(repo readers.MessageRepository, tc mainflux.ThingsServiceClient, cfg config, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.port)
|
||||
if cfg.serverCert != "" || cfg.serverKey != "" {
|
||||
logger.Info(fmt.Sprintf("Mongo reader service started using https on port %s with cert %s key %s",
|
||||
cfg.port, cfg.serverCert, cfg.serverKey))
|
||||
errs <- http.ListenAndServeTLS(p, cfg.serverCert, cfg.serverKey, api.MakeHandler(repo, tc, "mongodb-reader"))
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Mongo reader service started, exposed port %s", cfg.port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(repo, tc, "mongodb-reader"))
|
||||
}
|
||||
|
||||
+39
-64
@@ -6,22 +6,20 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/transformers/senml"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
"github.com/mainflux/mainflux/pkg/transformers/senml"
|
||||
"github.com/mainflux/mainflux/writers"
|
||||
"github.com/mainflux/mainflux/writers/api"
|
||||
"github.com/mainflux/mainflux/writers/mongodb"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
@@ -30,31 +28,34 @@ import (
|
||||
const (
|
||||
svcName = "mongodb-writer"
|
||||
|
||||
defNatsURL = nats.DefaultURL
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defDBName = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "27017"
|
||||
defChanCfgPath = "/config/channels.toml"
|
||||
defLogLevel = "error"
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defPort = "8180"
|
||||
defDB = "mainflux"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "27017"
|
||||
defSubjectsCfgPath = "/config/subjects.toml"
|
||||
defContentType = "application/senml+json"
|
||||
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_MONGO_WRITER_LOG_LEVEL"
|
||||
envPort = "MF_MONGO_WRITER_PORT"
|
||||
envDBName = "MF_MONGO_WRITER_DB_NAME"
|
||||
envDBHost = "MF_MONGO_WRITER_DB_HOST"
|
||||
envDBPort = "MF_MONGO_WRITER_DB_PORT"
|
||||
envChanCfgPath = "MF_MONGO_WRITER_CHANNELS_CONFIG"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_MONGO_WRITER_LOG_LEVEL"
|
||||
envPort = "MF_MONGO_WRITER_PORT"
|
||||
envDB = "MF_MONGO_WRITER_DB"
|
||||
envDBHost = "MF_MONGO_WRITER_DB_HOST"
|
||||
envDBPort = "MF_MONGO_WRITER_DB_PORT"
|
||||
envSubjectsCfgPath = "MF_MONGO_WRITER_SUBJECTS_CONFIG"
|
||||
envContentType = "MF_MONGO_WRITER_CONTENT_TYPE"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
dbName string
|
||||
dbHost string
|
||||
dbPort string
|
||||
channels map[string]bool
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
dbName string
|
||||
dbHost string
|
||||
dbPort string
|
||||
subjectsCfgPath string
|
||||
contentType string
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -65,12 +66,12 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
nc, err := nats.Connect(cfg.natsURL)
|
||||
pubSub, err := nats.NewPubSub(cfg.natsURL, "", logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
defer pubSub.Close()
|
||||
|
||||
addr := fmt.Sprintf("mongodb://%s:%s", cfg.dbHost, cfg.dbPort)
|
||||
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(addr))
|
||||
@@ -85,8 +86,9 @@ func main() {
|
||||
counter, latency := makeMetrics()
|
||||
repo = api.LoggingMiddleware(repo, logger)
|
||||
repo = api.MetricsMiddleware(repo, counter, latency)
|
||||
st := senml.New()
|
||||
if err := writers.Start(nc, repo, st, svcName, cfg.channels, logger); err != nil {
|
||||
st := senml.New(cfg.contentType)
|
||||
|
||||
if err := writers.Start(pubSub, repo, st, svcName, cfg.subjectsCfgPath, logger); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to start MongoDB writer: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -105,45 +107,18 @@ func main() {
|
||||
}
|
||||
|
||||
func loadConfigs() config {
|
||||
chanCfgPath := mainflux.Env(envChanCfgPath, defChanCfgPath)
|
||||
return config{
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbName: mainflux.Env(envDBName, defDBName),
|
||||
dbHost: mainflux.Env(envDBHost, defDBHost),
|
||||
dbPort: mainflux.Env(envDBPort, defDBPort),
|
||||
channels: loadChansConfig(chanCfgPath),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbName: mainflux.Env(envDB, defDB),
|
||||
dbHost: mainflux.Env(envDBHost, defDBHost),
|
||||
dbPort: mainflux.Env(envDBPort, defDBPort),
|
||||
subjectsCfgPath: mainflux.Env(envSubjectsCfgPath, defSubjectsCfgPath),
|
||||
contentType: mainflux.Env(envContentType, defContentType),
|
||||
}
|
||||
}
|
||||
|
||||
type channels struct {
|
||||
List []string `toml:"filter"`
|
||||
}
|
||||
|
||||
type chanConfig struct {
|
||||
Channels channels `toml:"channels"`
|
||||
}
|
||||
|
||||
func loadChansConfig(chanConfigPath string) map[string]bool {
|
||||
data, err := ioutil.ReadFile(chanConfigPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var chanCfg chanConfig
|
||||
if err := toml.Unmarshal(data, &chanCfg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
chans := map[string]bool{}
|
||||
for _, ch := range chanCfg.Channels.List {
|
||||
chans[ch] = true
|
||||
}
|
||||
|
||||
return chans
|
||||
}
|
||||
|
||||
func makeMetrics() (*kitprometheus.Counter, *kitprometheus.Summary) {
|
||||
counter := kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "mongodb",
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
mflog "github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/mqtt"
|
||||
mqttredis "github.com/mainflux/mainflux/mqtt/redis"
|
||||
"github.com/mainflux/mainflux/pkg/auth"
|
||||
"github.com/mainflux/mainflux/pkg/messaging"
|
||||
mqttpub "github.com/mainflux/mainflux/pkg/messaging/mqtt"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/auth/grpc"
|
||||
mp "github.com/mainflux/mproxy/pkg/mqtt"
|
||||
"github.com/mainflux/mproxy/pkg/session"
|
||||
ws "github.com/mainflux/mproxy/pkg/websocket"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
// Logging
|
||||
defLogLevel = "error"
|
||||
envLogLevel = "MF_MQTT_ADAPTER_LOG_LEVEL"
|
||||
// MQTT
|
||||
defMQTTPort = "1883"
|
||||
defMQTTTargetHost = "0.0.0.0"
|
||||
defMQTTTargetPort = "1883"
|
||||
defMQTTForwarderTimeout = "30s" // 30 seconds
|
||||
envMQTTPort = "MF_MQTT_ADAPTER_MQTT_PORT"
|
||||
envMQTTTargetHost = "MF_MQTT_ADAPTER_MQTT_TARGET_HOST"
|
||||
envMQTTTargetPort = "MF_MQTT_ADAPTER_MQTT_TARGET_PORT"
|
||||
envMQTTForwarderTimeout = "MF_MQTT_ADAPTER_FORWARDER_TIMEOUT"
|
||||
// HTTP
|
||||
defHTTPPort = "8080"
|
||||
defHTTPTargetHost = "localhost"
|
||||
defHTTPTargetPort = "8080"
|
||||
defHTTPTargetPath = "/mqtt"
|
||||
envHTTPPort = "MF_MQTT_ADAPTER_WS_PORT"
|
||||
envHTTPTargetHost = "MF_MQTT_ADAPTER_WS_TARGET_HOST"
|
||||
envHTTPTargetPort = "MF_MQTT_ADAPTER_WS_TARGET_PORT"
|
||||
envHTTPTargetPath = "MF_MQTT_ADAPTER_WS_TARGET_PATH"
|
||||
// Things
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1s"
|
||||
envThingsAuthURL = "MF_THINGS_AUTH_GRPC_URL"
|
||||
envThingsAuthTimeout = "MF_THINGS_AUTH_GRPC_TIMEOUT"
|
||||
// Nats
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
// Jaeger
|
||||
defJaegerURL = ""
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
// TLS
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
envClientTLS = "MF_MQTT_ADAPTER_CLIENT_TLS"
|
||||
envCACerts = "MF_MQTT_ADAPTER_CA_CERTS"
|
||||
// Instance
|
||||
envInstance = "MF_MQTT_ADAPTER_INSTANCE"
|
||||
defInstance = ""
|
||||
// ES
|
||||
envESURL = "MF_MQTT_ADAPTER_ES_URL"
|
||||
envESPass = "MF_MQTT_ADAPTER_ES_PASS"
|
||||
envESDB = "MF_MQTT_ADAPTER_ES_DB"
|
||||
defESURL = "localhost:6379"
|
||||
defESPass = ""
|
||||
defESDB = "0"
|
||||
// Auth cache
|
||||
envAuthCacheURL = "MF_AUTH_CACHE_URL"
|
||||
envAuthCachePass = "MF_AUTH_CACHE_PASS"
|
||||
envAuthCacheDB = "MF_AUTH_CACHE_DB"
|
||||
defAuthcacheURL = "localhost:6379"
|
||||
defAuthCachePass = ""
|
||||
defAuthCacheDB = "0"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
mqttPort string
|
||||
mqttTargetHost string
|
||||
mqttTargetPort string
|
||||
mqttForwarderTimeout time.Duration
|
||||
httpPort string
|
||||
httpTargetHost string
|
||||
httpTargetPort string
|
||||
httpTargetPath string
|
||||
jaegerURL string
|
||||
logLevel string
|
||||
thingsURL string
|
||||
thingsAuthURL string
|
||||
thingsAuthTimeout time.Duration
|
||||
natsURL string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
instance string
|
||||
esURL string
|
||||
esPass string
|
||||
esDB string
|
||||
authURL string
|
||||
authPass string
|
||||
authDB string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, err := mflog.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
conn := connectToThings(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
ec := connectToRedis(cfg.esURL, cfg.esPass, cfg.esDB, logger)
|
||||
defer ec.Close()
|
||||
|
||||
nps, err := nats.NewPubSub(cfg.natsURL, "mqtt", logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nps.Close()
|
||||
|
||||
mp, err := mqttpub.NewPublisher(fmt.Sprintf("%s:%s", cfg.mqttTargetHost, cfg.mqttTargetPort), cfg.mqttForwarderTimeout)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create MQTT publisher: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fwd := mqtt.NewForwarder(nats.SubjectAllChannels, logger)
|
||||
if err := fwd.Forward(nps, mp); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to forward NATS messages: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
np, err := nats.NewPublisher(cfg.natsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer np.Close()
|
||||
|
||||
es := mqttredis.NewEventStore(ec, cfg.instance)
|
||||
|
||||
ac := connectToRedis(cfg.authURL, cfg.authPass, cfg.authDB, logger)
|
||||
defer ac.Close()
|
||||
|
||||
thingsTracer, thingsCloser := initJaeger("things", cfg.jaegerURL, logger)
|
||||
defer thingsCloser.Close()
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsAuthTimeout)
|
||||
|
||||
authClient := auth.New(ac, tc)
|
||||
|
||||
// Event handler for MQTT hooks
|
||||
h := mqtt.NewHandler([]messaging.Publisher{np}, es, logger, authClient)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
logger.Info(fmt.Sprintf("Starting MQTT proxy on port %s", cfg.mqttPort))
|
||||
go proxyMQTT(cfg, logger, h, errs)
|
||||
|
||||
logger.Info(fmt.Sprintf("Starting MQTT over WS proxy on port %s", cfg.httpPort))
|
||||
go proxyWS(cfg, logger, h, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("mProxy terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
|
||||
mqttTimeout, err := time.ParseDuration(mainflux.Env(envMQTTForwarderTimeout, defMQTTForwarderTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
|
||||
return config{
|
||||
mqttPort: mainflux.Env(envMQTTPort, defMQTTPort),
|
||||
mqttTargetHost: mainflux.Env(envMQTTTargetHost, defMQTTTargetHost),
|
||||
mqttTargetPort: mainflux.Env(envMQTTTargetPort, defMQTTTargetPort),
|
||||
mqttForwarderTimeout: mqttTimeout,
|
||||
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
httpTargetHost: mainflux.Env(envHTTPTargetHost, defHTTPTargetHost),
|
||||
httpTargetPort: mainflux.Env(envHTTPTargetPort, defHTTPTargetPort),
|
||||
httpTargetPath: mainflux.Env(envHTTPTargetPath, defHTTPTargetPath),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: authTimeout,
|
||||
thingsURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
instance: mainflux.Env(envInstance, defInstance),
|
||||
esURL: mainflux.Env(envESURL, defESURL),
|
||||
esPass: mainflux.Env(envESPass, defESPass),
|
||||
esDB: mainflux.Env(envESDB, defESDB),
|
||||
authURL: mainflux.Env(envAuthCacheURL, defAuthcacheURL),
|
||||
authPass: mainflux.Env(envAuthCachePass, defAuthCachePass),
|
||||
authDB: mainflux.Env(envAuthCacheDB, defAuthCacheDB),
|
||||
}
|
||||
}
|
||||
|
||||
func initJaeger(svcName, url string, logger mflog.Logger) (opentracing.Tracer, io.Closer) {
|
||||
if url == "" {
|
||||
return opentracing.NoopTracer{}, ioutil.NopCloser(nil)
|
||||
}
|
||||
|
||||
tracer, closer, err := jconfig.Configuration{
|
||||
ServiceName: svcName,
|
||||
Sampler: &jconfig.SamplerConfig{
|
||||
Type: "const",
|
||||
Param: 1,
|
||||
},
|
||||
Reporter: &jconfig.ReporterConfig{
|
||||
LocalAgentHostPort: url,
|
||||
LogSpans: true,
|
||||
},
|
||||
}.NewTracer()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to init Jaeger client: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return tracer, closer
|
||||
}
|
||||
|
||||
func connectToThings(cfg config, logger mflog.Logger) *grpc.ClientConn {
|
||||
var opts []grpc.DialOption
|
||||
if cfg.clientTLS {
|
||||
if cfg.caCerts != "" {
|
||||
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(tpc))
|
||||
}
|
||||
} else {
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsAuthURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
func connectToRedis(redisURL, redisPass, redisDB string, logger mflog.Logger) *redis.Client {
|
||||
db, err := strconv.Atoi(redisDB)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to redis: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return redis.NewClient(&redis.Options{
|
||||
Addr: redisURL,
|
||||
Password: redisPass,
|
||||
DB: db,
|
||||
})
|
||||
}
|
||||
|
||||
func proxyMQTT(cfg config, logger mflog.Logger, handler session.Handler, errs chan error) {
|
||||
address := fmt.Sprintf(":%s", cfg.mqttPort)
|
||||
target := fmt.Sprintf("%s:%s", cfg.mqttTargetHost, cfg.mqttTargetPort)
|
||||
mp := mp.New(address, target, handler, logger)
|
||||
|
||||
errs <- mp.Listen()
|
||||
}
|
||||
func proxyWS(cfg config, logger mflog.Logger, handler session.Handler, errs chan error) {
|
||||
target := fmt.Sprintf("%s:%s", cfg.httpTargetHost, cfg.httpTargetPort)
|
||||
wp := ws.New(target, cfg.httpTargetPath, "ws", handler, logger)
|
||||
http.Handle("/mqtt", wp.Handler())
|
||||
|
||||
errs <- wp.Listen(cfg.httpPort)
|
||||
}
|
||||
+34
-70
@@ -5,9 +5,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -20,23 +18,24 @@ import (
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/opcua"
|
||||
"github.com/mainflux/mainflux/opcua/api"
|
||||
"github.com/mainflux/mainflux/opcua/db"
|
||||
"github.com/mainflux/mainflux/opcua/gopcua"
|
||||
pub "github.com/mainflux/mainflux/opcua/nats"
|
||||
"github.com/mainflux/mainflux/opcua/redis"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
defHTTPPort = "8188"
|
||||
defLogLevel = "error"
|
||||
defHTTPPort = "8180"
|
||||
defOPCIntervalMs = "1000"
|
||||
defOPCPolicy = ""
|
||||
defOPCMode = ""
|
||||
defOPCCertFile = ""
|
||||
defOPCKeyFile = ""
|
||||
defNatsURL = nats.DefaultURL
|
||||
defLogLevel = "debug"
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defESURL = "localhost:6379"
|
||||
defESPass = ""
|
||||
defESDB = "0"
|
||||
@@ -44,10 +43,10 @@ const (
|
||||
defRouteMapURL = "localhost:6379"
|
||||
defRouteMapPass = ""
|
||||
defRouteMapDB = "0"
|
||||
defNodesConfig = "/config/nodes.csv"
|
||||
|
||||
envHTTPPort = "MF_OPCUA_ADAPTER_HTTP_PORT"
|
||||
envLogLevel = "MF_OPCUA_ADAPTER_LOG_LEVEL"
|
||||
envHTTPPort = "MF_OPCUA_ADAPTER_HTTP_PORT"
|
||||
envOPCIntervalMs = "MF_OPCUA_ADAPTER_INTERVAL_MS"
|
||||
envOPCPolicy = "MF_OPCUA_ADAPTER_POLICY"
|
||||
envOPCMode = "MF_OPCUA_ADAPTER_MODE"
|
||||
envOPCCertFile = "MF_OPCUA_ADAPTER_CERT_FILE"
|
||||
@@ -60,13 +59,10 @@ const (
|
||||
envRouteMapURL = "MF_OPCUA_ADAPTER_ROUTE_MAP_URL"
|
||||
envRouteMapPass = "MF_OPCUA_ADAPTER_ROUTE_MAP_PASS"
|
||||
envRouteMapDB = "MF_OPCUA_ADAPTER_ROUTE_MAP_DB"
|
||||
envNodesConfig = "MF_OPCUA_ADAPTER_CONFIG_FILE"
|
||||
|
||||
thingsRMPrefix = "thing"
|
||||
channelsRMPrefix = "channel"
|
||||
connectionRMPrefix = "connection"
|
||||
|
||||
columns = 2
|
||||
)
|
||||
|
||||
type config struct {
|
||||
@@ -81,7 +77,6 @@ type config struct {
|
||||
routeMapURL string
|
||||
routeMapPass string
|
||||
routeMapDB string
|
||||
nodesConfig string
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -92,9 +87,6 @@ func main() {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
natsConn := connectToNATS(cfg.natsURL, logger)
|
||||
defer natsConn.Close()
|
||||
|
||||
rmConn := connectToRedis(cfg.routeMapURL, cfg.routeMapPass, cfg.routeMapDB, logger)
|
||||
defer rmConn.Close()
|
||||
|
||||
@@ -105,12 +97,18 @@ func main() {
|
||||
esConn := connectToRedis(cfg.esURL, cfg.esPass, cfg.esDB, logger)
|
||||
defer esConn.Close()
|
||||
|
||||
publisher := pub.NewMessagePublisher(natsConn)
|
||||
pubSub, err := nats.NewPubSub(cfg.natsURL, "", logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pubSub.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
sub := gopcua.NewSubscriber(ctx, publisher, thingRM, chanRM, connRM, logger)
|
||||
sub := gopcua.NewSubscriber(ctx, pubSub, thingRM, chanRM, connRM, logger)
|
||||
browser := gopcua.NewBrowser(ctx, logger)
|
||||
|
||||
svc := opcua.New(sub, thingRM, chanRM, connRM, cfg.opcuaConfig, logger)
|
||||
svc := opcua.New(sub, browser, thingRM, chanRM, connRM, cfg.opcuaConfig, logger)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
@@ -128,12 +126,12 @@ func main() {
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
go subscribeToNodesFromFile(sub, cfg.nodesConfig, cfg.opcuaConfig, logger)
|
||||
go subscribeToStoredSubs(sub, cfg.opcuaConfig, logger)
|
||||
go subscribeToThingsES(svc, esConn, cfg.esConsumerName, logger)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(cfg, logger, errs)
|
||||
go startHTTPServer(svc, cfg, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
@@ -147,6 +145,7 @@ func main() {
|
||||
|
||||
func loadConfig() config {
|
||||
oc := opcua.Config{
|
||||
Interval: mainflux.Env(envOPCIntervalMs, defOPCIntervalMs),
|
||||
Policy: mainflux.Env(envOPCPolicy, defOPCPolicy),
|
||||
Mode: mainflux.Env(envOPCMode, defOPCMode),
|
||||
CertFile: mainflux.Env(envOPCCertFile, defOPCCertFile),
|
||||
@@ -164,21 +163,9 @@ func loadConfig() config {
|
||||
routeMapURL: mainflux.Env(envRouteMapURL, defRouteMapURL),
|
||||
routeMapPass: mainflux.Env(envRouteMapPass, defRouteMapPass),
|
||||
routeMapDB: mainflux.Env(envRouteMapDB, defRouteMapDB),
|
||||
nodesConfig: mainflux.Env(envNodesConfig, defNodesConfig),
|
||||
}
|
||||
}
|
||||
|
||||
func connectToNATS(url string, logger logger.Logger) *nats.Conn {
|
||||
conn, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger.Info("Connected to NATS")
|
||||
return conn
|
||||
}
|
||||
|
||||
func connectToRedis(redisURL, redisPass, redisDB string, logger logger.Logger) *r.Client {
|
||||
db, err := strconv.Atoi(redisDB)
|
||||
if err != nil {
|
||||
@@ -193,44 +180,21 @@ func connectToRedis(redisURL, redisPass, redisDB string, logger logger.Logger) *
|
||||
})
|
||||
}
|
||||
|
||||
func subscribeToNodesFromFile(sub opcua.Subscriber, nodes string, cfg opcua.Config, logger logger.Logger) {
|
||||
if _, err := os.Stat(nodes); os.IsNotExist(err) {
|
||||
logger.Warn(fmt.Sprintf("Config file not found: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(nodes, os.O_RDONLY, os.ModePerm)
|
||||
func subscribeToStoredSubs(sub opcua.Subscriber, cfg opcua.Config, logger logger.Logger) {
|
||||
// Get all stored subscriptions
|
||||
nodes, err := db.ReadAll()
|
||||
if err != nil {
|
||||
logger.Warn(fmt.Sprintf("Failed to open config file: %s", err))
|
||||
return
|
||||
logger.Warn(fmt.Sprintf("Read stored subscriptions failed: %s", err))
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
for {
|
||||
l, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
logger.Warn(fmt.Sprintf("Failed to read config file: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(l) < columns {
|
||||
logger.Warn("Empty or incomplete line found in file")
|
||||
return
|
||||
}
|
||||
|
||||
cfg.ServerURI = l[0]
|
||||
cfg.NodeID = l[1]
|
||||
go subscribe(sub, cfg, logger)
|
||||
}
|
||||
}
|
||||
|
||||
func subscribe(sub opcua.Subscriber, cfg opcua.Config, logger logger.Logger) {
|
||||
if err := sub.Subscribe(cfg); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Subscription failed: %s", err))
|
||||
for _, n := range nodes {
|
||||
cfg.ServerURI = n.ServerURI
|
||||
cfg.NodeID = n.NodeID
|
||||
go func() {
|
||||
if err := sub.Subscribe(cfg); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Subscription failed: %s", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,8 +216,8 @@ func newRouteMapRepositoy(client *r.Client, prefix string, logger logger.Logger)
|
||||
return redis.NewRouteMapRepository(client, prefix)
|
||||
}
|
||||
|
||||
func startHTTPServer(cfg config, logger logger.Logger, errs chan error) {
|
||||
func startHTTPServer(svc opcua.Service, cfg config, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.httpPort)
|
||||
logger.Info(fmt.Sprintf("opcua-adapter service started, exposed port %s", cfg.httpPort))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler())
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
|
||||
}
|
||||
|
||||
+62
-55
@@ -31,53 +31,53 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
svcName = "postgres-writer"
|
||||
svcName = "postgres-reader"
|
||||
sep = ","
|
||||
|
||||
defThingsURL = "localhost:8183"
|
||||
defLogLevel = "debug"
|
||||
defPort = "9204"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "messages"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defJaegerURL = ""
|
||||
defThingsTimeout = "1" // in seconds
|
||||
defLogLevel = "error"
|
||||
defPort = "8180"
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDB = "mainflux"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defJaegerURL = ""
|
||||
defThingsAuthURL = "localhost:8181"
|
||||
defThingsAuthTimeout = "1s"
|
||||
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
envLogLevel = "MF_POSTGRES_READER_LOG_LEVEL"
|
||||
envPort = "MF_POSTGRES_READER_PORT"
|
||||
envClientTLS = "MF_POSTGRES_READER_CLIENT_TLS"
|
||||
envCACerts = "MF_POSTGRES_READER_CA_CERTS"
|
||||
envDBHost = "MF_POSTGRES_READER_DB_HOST"
|
||||
envDBPort = "MF_POSTGRES_READER_DB_PORT"
|
||||
envDBUser = "MF_POSTGRES_READER_DB_USER"
|
||||
envDBPass = "MF_POSTGRES_READER_DB_PASS"
|
||||
envDBName = "MF_POSTGRES_READER_DB_NAME"
|
||||
envDBSSLMode = "MF_POSTGRES_READER_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_POSTGRES_READER_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_POSTGRES_READER_DB_SSL_KEY"
|
||||
envDBSSLRootCert = "MF_POSTGRES_READER_DB_SSL_ROOT_CERT"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsTimeout = "MF_POSTGRES_READER_THINGS_TIMEOUT"
|
||||
envLogLevel = "MF_POSTGRES_READER_LOG_LEVEL"
|
||||
envPort = "MF_POSTGRES_READER_PORT"
|
||||
envClientTLS = "MF_POSTGRES_READER_CLIENT_TLS"
|
||||
envCACerts = "MF_POSTGRES_READER_CA_CERTS"
|
||||
envDBHost = "MF_POSTGRES_READER_DB_HOST"
|
||||
envDBPort = "MF_POSTGRES_READER_DB_PORT"
|
||||
envDBUser = "MF_POSTGRES_READER_DB_USER"
|
||||
envDBPass = "MF_POSTGRES_READER_DB_PASS"
|
||||
envDB = "MF_POSTGRES_READER_DB"
|
||||
envDBSSLMode = "MF_POSTGRES_READER_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_POSTGRES_READER_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_POSTGRES_READER_DB_SSL_KEY"
|
||||
envDBSSLRootCert = "MF_POSTGRES_READER_DB_SSL_ROOT_CERT"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsAuthURL = "MF_THINGS_AUTH_GRPC_URL"
|
||||
envThingsAuthTimeout = "MF_THINGS_AUTH_GRPC_TIMEOUT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
thingsURL string
|
||||
logLevel string
|
||||
port string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
dbConfig postgres.Config
|
||||
jaegerURL string
|
||||
thingsTimeout time.Duration
|
||||
logLevel string
|
||||
port string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
dbConfig postgres.Config
|
||||
jaegerURL string
|
||||
thingsAuthURL string
|
||||
thingsAuthTimeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -94,7 +94,7 @@ func main() {
|
||||
thingsTracer, thingsCloser := initJaeger("things", cfg.jaegerURL, logger)
|
||||
defer thingsCloser.Close()
|
||||
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsTimeout)
|
||||
tc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsAuthTimeout)
|
||||
|
||||
db := connectToDB(cfg.dbConfig, logger)
|
||||
defer db.Close()
|
||||
@@ -112,7 +112,7 @@ func main() {
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Postgres writer service terminated: %s", err))
|
||||
logger.Error(fmt.Sprintf("Postgres reader service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
@@ -121,25 +121,32 @@ func loadConfig() config {
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Name: mainflux.Env(envDBName, defDBName),
|
||||
Name: mainflux.Env(envDB, defDB),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
SSLRootCert: mainflux.Env(envDBSSLRootCert, defDBSSLRootCert),
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsTimeout, defThingsTimeout), 10, 64)
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsTimeout, err.Error())
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
authTimeout, err := time.ParseDuration(mainflux.Env(envThingsAuthTimeout, defThingsAuthTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsAuthTimeout, err.Error())
|
||||
}
|
||||
|
||||
return config{
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbConfig: dbConfig,
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsTimeout: time.Duration(timeout) * time.Second,
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
dbConfig: dbConfig,
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsAuthURL: mainflux.Env(envThingsAuthURL, defThingsAuthURL),
|
||||
thingsAuthTimeout: authTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +199,7 @@ func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
conn, err := grpc.Dial(cfg.thingsAuthURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
@@ -207,13 +214,13 @@ func newService(db *sqlx.DB, logger logger.Logger) readers.MessageRepository {
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "postgres",
|
||||
Subsystem: "message_writer",
|
||||
Subsystem: "message_reader",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "postgres",
|
||||
Subsystem: "message_writer",
|
||||
Subsystem: "message_reader",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
|
||||
+51
-83
@@ -5,23 +5,21 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/transformers/senml"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
"github.com/mainflux/mainflux/pkg/transformers/senml"
|
||||
"github.com/mainflux/mainflux/writers"
|
||||
"github.com/mainflux/mainflux/writers/api"
|
||||
"github.com/mainflux/mainflux/writers/postgres"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@@ -29,41 +27,44 @@ const (
|
||||
svcName = "postgres-writer"
|
||||
sep = ","
|
||||
|
||||
defNatsURL = nats.DefaultURL
|
||||
defLogLevel = "error"
|
||||
defPort = "9104"
|
||||
defDBHost = "postgres"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "messages"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defChanCfgPath = "/config/channels.toml"
|
||||
defLogLevel = "error"
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defPort = "8180"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDB = "mainflux"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
defDBSSLRootCert = ""
|
||||
defSubjectsCfgPath = "/config/subjects.toml"
|
||||
defContentType = "application/senml+json"
|
||||
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_POSTGRES_WRITER_LOG_LEVEL"
|
||||
envPort = "MF_POSTGRES_WRITER_PORT"
|
||||
envDBHost = "MF_POSTGRES_WRITER_DB_HOST"
|
||||
envDBPort = "MF_POSTGRES_WRITER_DB_PORT"
|
||||
envDBUser = "MF_POSTGRES_WRITER_DB_USER"
|
||||
envDBPass = "MF_POSTGRES_WRITER_DB_PASS"
|
||||
envDBName = "MF_POSTGRES_WRITER_DB_NAME"
|
||||
envDBSSLMode = "MF_POSTGRES_WRITER_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_POSTGRES_WRITER_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_POSTGRES_WRITER_DB_SSL_KEY"
|
||||
envDBSSLRootCert = "MF_POSTGRES_WRITER_DB_SSL_ROOT_CERT"
|
||||
envChanCfgPath = "MF_POSTGRES_WRITER_CHANNELS_CONFIG"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envLogLevel = "MF_POSTGRES_WRITER_LOG_LEVEL"
|
||||
envPort = "MF_POSTGRES_WRITER_PORT"
|
||||
envDBHost = "MF_POSTGRES_WRITER_DB_HOST"
|
||||
envDBPort = "MF_POSTGRES_WRITER_DB_PORT"
|
||||
envDBUser = "MF_POSTGRES_WRITER_DB_USER"
|
||||
envDBPass = "MF_POSTGRES_WRITER_DB_PASS"
|
||||
envDB = "MF_POSTGRES_WRITER_DB"
|
||||
envDBSSLMode = "MF_POSTGRES_WRITER_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_POSTGRES_WRITER_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_POSTGRES_WRITER_DB_SSL_KEY"
|
||||
envDBSSLRootCert = "MF_POSTGRES_WRITER_DB_SSL_ROOT_CERT"
|
||||
envSubjectsCfgPath = "MF_POSTGRES_WRITER_SUBJECTS_CONFIG"
|
||||
envContentType = "MF_POSTGRES_WRITER_CONTENT_TYPE"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
dbConfig postgres.Config
|
||||
channels map[string]bool
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
subjectsCfgPath string
|
||||
contentType string
|
||||
dbConfig postgres.Config
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -74,15 +75,19 @@ func main() {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
nc := connectToNATS(cfg.natsURL, logger)
|
||||
defer nc.Close()
|
||||
pubSub, err := nats.NewPubSub(cfg.natsURL, "", logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pubSub.Close()
|
||||
|
||||
db := connectToDB(cfg.dbConfig, logger)
|
||||
defer db.Close()
|
||||
|
||||
repo := newService(db, logger)
|
||||
st := senml.New()
|
||||
if err = writers.Start(nc, repo, st, svcName, cfg.channels, logger); err != nil {
|
||||
st := senml.New(cfg.contentType)
|
||||
if err = writers.Start(pubSub, repo, st, svcName, cfg.subjectsCfgPath, logger); err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to create Postgres writer: %s", err))
|
||||
}
|
||||
|
||||
@@ -101,13 +106,12 @@ func main() {
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
chanCfgPath := mainflux.Env(envChanCfgPath, defChanCfgPath)
|
||||
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),
|
||||
Name: mainflux.Env(envDB, defDB),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
@@ -115,51 +119,15 @@ func loadConfig() config {
|
||||
}
|
||||
|
||||
return config{
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
dbConfig: dbConfig,
|
||||
channels: loadChansConfig(chanCfgPath),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
subjectsCfgPath: mainflux.Env(envSubjectsCfgPath, defSubjectsCfgPath),
|
||||
contentType: mainflux.Env(envContentType, defContentType),
|
||||
dbConfig: dbConfig,
|
||||
}
|
||||
}
|
||||
|
||||
type channels struct {
|
||||
List []string `toml:"filter"`
|
||||
}
|
||||
|
||||
type chanConfig struct {
|
||||
Channels channels `toml:"channels"`
|
||||
}
|
||||
|
||||
func loadChansConfig(chanConfigPath string) map[string]bool {
|
||||
data, err := ioutil.ReadFile(chanConfigPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var chanCfg chanConfig
|
||||
if err := toml.Unmarshal(data, &chanCfg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
chans := map[string]bool{}
|
||||
for _, ch := range chanCfg.Channels.List {
|
||||
chans[ch] = true
|
||||
}
|
||||
|
||||
return chans
|
||||
}
|
||||
|
||||
func connectToNATS(url string, logger logger.Logger) *nats.Conn {
|
||||
nc, err := nats.Connect(url)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
func connectToDB(dbConfig postgres.Config, logger logger.Logger) *sqlx.DB {
|
||||
db, err := postgres.Connect(dbConfig)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/errors"
|
||||
mfSDK "github.com/mainflux/mainflux/pkg/sdk/go"
|
||||
"github.com/mainflux/mainflux/provision"
|
||||
"github.com/mainflux/mainflux/provision/api"
|
||||
)
|
||||
|
||||
const (
|
||||
defLogLevel = "debug"
|
||||
defConfigFile = "config.toml"
|
||||
defTLS = "false"
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defThingsLocation = "http://localhost"
|
||||
defUsersLocation = "http://localhost"
|
||||
defMQTTURL = "localhost:1883"
|
||||
defHTTPPort = "8091"
|
||||
defMfUser = "test@example.com"
|
||||
defMfPass = "test"
|
||||
defMfAPIKey = ""
|
||||
defMfBSURL = "http://localhost:8202/things/configs"
|
||||
defMfWhiteListURL = "http://localhost:8202/things/state"
|
||||
defMfCertsURL = "http://localhost:8204"
|
||||
defProvisionCerts = "false"
|
||||
defProvisionBS = "true"
|
||||
defBSAutoWhitelist = "true"
|
||||
defBSContent = ""
|
||||
defCertsHoursValid = "2400h"
|
||||
defCertsKeyBits = "4096"
|
||||
|
||||
envConfigFile = "MF_PROVISION_CONFIG_FILE"
|
||||
envLogLevel = "MF_PROVISION_LOG_LEVEL"
|
||||
envHTTPPort = "MF_PROVISION_HTTP_PORT"
|
||||
envTLS = "MF_PROVISION_ENV_CLIENTS_TLS"
|
||||
envServerCert = "MF_PROVISION_SERVER_CERT"
|
||||
envServerKey = "MF_PROVISION_SERVER_KEY"
|
||||
envMQTTURL = "MF_PROVISION_MQTT_URL"
|
||||
envUsersLocation = "MF_PROVISION_USERS_LOCATION"
|
||||
envThingsLocation = "MF_PROVISION_THINGS_LOCATION"
|
||||
envMfUser = "MF_PROVISION_USER"
|
||||
envMfPass = "MF_PROVISION_PASS"
|
||||
envMfAPIKey = "MF_PROVISION_API_KEY"
|
||||
envMfCertsURL = "MF_PROVISION_CERTS_SVC_URL"
|
||||
envProvisionCerts = "MF_PROVISION_X509_PROVISIONING"
|
||||
envMfBSURL = "MF_PROVISION_BS_SVC_URL"
|
||||
envMfBSWhiteListURL = "MF_PROVISION_BS_SVC_WHITELIST_URL"
|
||||
envProvisionBS = "MF_PROVISION_BS_CONFIG_PROVISIONING"
|
||||
envBSAutoWhiteList = "MF_PROVISION_BS_AUTO_WHITELIST"
|
||||
envBSContent = "MF_PROVISION_BS_CONTENT"
|
||||
envCertsHoursValid = "MF_PROVISION_CERTS_HOURS_VALID"
|
||||
envCertsKeyBits = "MF_PROVISION_CERTS_RSA_BITS"
|
||||
)
|
||||
|
||||
var (
|
||||
errMissingConfigFile = errors.New("missing config file setting")
|
||||
errFailLoadingConfigFile = errors.New("failed to load config from file")
|
||||
errFailGettingAutoWhiteList = errors.New("failed to get auto whitelist setting")
|
||||
errFailGettingCertSettings = errors.New("failed to get certificate file setting")
|
||||
errFailGettingTLSConf = errors.New("failed to get TLS setting")
|
||||
errFailGettingProvBS = errors.New("failed to get BS url setting")
|
||||
errFailSettingKeyBits = errors.New("failed to set rsa number of bits")
|
||||
errFailedToReadBootstrapContent = errors.New("failed to read bootstrap content from envs")
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
logger, err := logger.New(os.Stdout, cfg.Server.LogLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
if cfgFromFile, err := loadConfigFromFile(cfg.File); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Continue with settings from env, failed to load from: %s: %s", cfg.File, err))
|
||||
} else {
|
||||
// Merge environment variables and file settings.
|
||||
mergeConfigs(&cfgFromFile, &cfg)
|
||||
cfg = cfgFromFile
|
||||
logger.Info("Continue with settings from file: " + cfg.File)
|
||||
}
|
||||
|
||||
SDKCfg := mfSDK.Config{
|
||||
BaseURL: cfg.Server.ThingsLocation,
|
||||
BootstrapURL: cfg.Server.MfBSURL,
|
||||
CertsURL: cfg.Server.MfCertsURL,
|
||||
HTTPAdapterPrefix: "http",
|
||||
MsgContentType: "application/json",
|
||||
TLSVerification: cfg.Server.TLS,
|
||||
}
|
||||
SDK := mfSDK.NewSDK(SDKCfg)
|
||||
|
||||
svc := provision.New(cfg, SDK, logger)
|
||||
svc = api.NewLoggingMiddleware(svc, logger)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go startHTTPServer(svc, cfg, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Provision service terminated: %s", err))
|
||||
}
|
||||
|
||||
func startHTTPServer(svc provision.Service, cfg provision.Config, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", cfg.Server.HTTPPort)
|
||||
if cfg.Server.ServerCert != "" || cfg.Server.ServerKey != "" {
|
||||
logger.Info(fmt.Sprintf("Provision service started using https on port %s with cert %s key %s",
|
||||
cfg.Server.HTTPPort, cfg.Server.ServerCert, cfg.Server.ServerKey))
|
||||
errs <- http.ListenAndServeTLS(p, cfg.Server.ServerCert, cfg.Server.ServerKey, api.MakeHandler(svc))
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Provision service started using http on port %s", cfg.Server.HTTPPort))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
|
||||
}
|
||||
|
||||
func loadConfigFromFile(file string) (provision.Config, error) {
|
||||
_, err := os.Stat(file)
|
||||
if os.IsNotExist(err) {
|
||||
return provision.Config{}, errors.Wrap(errMissingConfigFile, err)
|
||||
}
|
||||
c, err := provision.Read(file)
|
||||
if err != nil {
|
||||
return provision.Config{}, errors.Wrap(errFailLoadingConfigFile, err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func loadConfig() (provision.Config, error) {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envTLS, defTLS))
|
||||
if err != nil {
|
||||
return provision.Config{}, errors.Wrap(errFailGettingTLSConf, err)
|
||||
}
|
||||
provisionX509, err := strconv.ParseBool(mainflux.Env(envProvisionCerts, defProvisionCerts))
|
||||
if err != nil {
|
||||
return provision.Config{}, errors.Wrap(errFailGettingCertSettings, err)
|
||||
}
|
||||
provisionBS, err := strconv.ParseBool(mainflux.Env(envProvisionBS, defProvisionBS))
|
||||
if err != nil {
|
||||
return provision.Config{}, errors.Wrap(errFailGettingProvBS, fmt.Errorf(" for %s", envProvisionBS))
|
||||
}
|
||||
|
||||
autoWhiteList, err := strconv.ParseBool(mainflux.Env(envBSAutoWhiteList, defBSAutoWhitelist))
|
||||
if err != nil {
|
||||
return provision.Config{}, errors.Wrap(errFailGettingAutoWhiteList, fmt.Errorf(" for %s", envBSAutoWhiteList))
|
||||
}
|
||||
if autoWhiteList && !provisionBS {
|
||||
return provision.Config{}, errors.New("Can't auto whitelist if auto config save is off")
|
||||
}
|
||||
keyBits, err := strconv.Atoi(mainflux.Env(envCertsKeyBits, defCertsKeyBits))
|
||||
if err != nil && provisionX509 == true {
|
||||
return provision.Config{}, errFailSettingKeyBits
|
||||
}
|
||||
|
||||
var content map[string]interface{}
|
||||
if c := mainflux.Env(envBSContent, defBSContent); c != "" {
|
||||
if err = json.Unmarshal([]byte(c), content); err != nil {
|
||||
return provision.Config{}, errFailedToReadBootstrapContent
|
||||
}
|
||||
}
|
||||
|
||||
cfg := provision.Config{
|
||||
Server: provision.ServiceConf{
|
||||
LogLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
ServerCert: mainflux.Env(envServerCert, defServerCert),
|
||||
ServerKey: mainflux.Env(envServerKey, defServerKey),
|
||||
HTTPPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
MfBSURL: mainflux.Env(envMfBSURL, defMfBSURL),
|
||||
MfWhiteListURL: mainflux.Env(envMfBSWhiteListURL, defMfWhiteListURL),
|
||||
MfCertsURL: mainflux.Env(envMfCertsURL, defMfCertsURL),
|
||||
MfUser: mainflux.Env(envMfUser, defMfUser),
|
||||
MfPass: mainflux.Env(envMfPass, defMfPass),
|
||||
MfAPIKey: mainflux.Env(envMfAPIKey, defMfAPIKey),
|
||||
ThingsLocation: mainflux.Env(envThingsLocation, defThingsLocation),
|
||||
UsersLocation: mainflux.Env(envUsersLocation, defUsersLocation),
|
||||
TLS: tls,
|
||||
},
|
||||
Certs: provision.Certs{
|
||||
HoursValid: mainflux.Env(envCertsHoursValid, defCertsHoursValid),
|
||||
KeyBits: keyBits,
|
||||
},
|
||||
Bootstrap: provision.Bootstrap{
|
||||
X509Provision: provisionX509,
|
||||
Provision: provisionBS,
|
||||
AutoWhiteList: autoWhiteList,
|
||||
Content: content,
|
||||
},
|
||||
|
||||
// This is default conf for provision if there is no config file
|
||||
Channels: []provision.Channel{
|
||||
{
|
||||
Name: "control-channel",
|
||||
Metadata: map[string]interface{}{"type": "control"},
|
||||
}, {
|
||||
Name: "data-channel",
|
||||
Metadata: map[string]interface{}{"type": "data"},
|
||||
},
|
||||
},
|
||||
Things: []provision.Thing{
|
||||
{
|
||||
Name: "thing",
|
||||
Metadata: map[string]interface{}{"external_id": "xxxxxx"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg.File = mainflux.Env(envConfigFile, defConfigFile)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func mergeConfigs(dst, src interface{}) interface{} {
|
||||
d := reflect.ValueOf(dst).Elem()
|
||||
s := reflect.ValueOf(src).Elem()
|
||||
|
||||
for i := 0; i < d.NumField(); i++ {
|
||||
dField := d.Field(i)
|
||||
sField := s.Field(i)
|
||||
switch dField.Kind() {
|
||||
case reflect.Struct:
|
||||
dst := dField.Addr().Interface()
|
||||
src := sField.Addr().Interface()
|
||||
m := mergeConfigs(dst, src)
|
||||
val := reflect.ValueOf(m).Elem().Interface()
|
||||
dField.Set(reflect.ValueOf(val))
|
||||
case reflect.Slice:
|
||||
case reflect.Bool:
|
||||
if dField.Interface() == false {
|
||||
dField.Set(reflect.ValueOf(sField.Interface()))
|
||||
}
|
||||
case reflect.Int:
|
||||
if dField.Interface() == 0 {
|
||||
dField.Set(reflect.ValueOf(sField.Interface()))
|
||||
}
|
||||
case reflect.String:
|
||||
if dField.Interface() == "" {
|
||||
dField.Set(reflect.ValueOf(sField.Interface()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
+21
-21
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/mainflux/mainflux"
|
||||
authapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
|
||||
"github.com/mainflux/mainflux/things"
|
||||
"github.com/mainflux/mainflux/things/api"
|
||||
authgrpcapi "github.com/mainflux/mainflux/things/api/auth/grpc"
|
||||
@@ -35,7 +36,6 @@ import (
|
||||
"github.com/mainflux/mainflux/things/postgres"
|
||||
rediscache "github.com/mainflux/mainflux/things/redis"
|
||||
localusers "github.com/mainflux/mainflux/things/users"
|
||||
"github.com/mainflux/mainflux/things/uuid"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
"google.golang.org/grpc"
|
||||
@@ -47,7 +47,7 @@ const (
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "things"
|
||||
defDB = "things"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
@@ -60,23 +60,23 @@ const (
|
||||
defESURL = "localhost:6379"
|
||||
defESPass = ""
|
||||
defESDB = "0"
|
||||
defHTTPPort = "8180"
|
||||
defAuthHTTPPort = "8989"
|
||||
defHTTPPort = "8182"
|
||||
defAuthHTTPPort = "8180"
|
||||
defAuthGRPCPort = "8181"
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defSingleUserEmail = ""
|
||||
defSingleUserToken = ""
|
||||
defJaegerURL = ""
|
||||
defAuthURL = "localhost:8181"
|
||||
defAuthTimeout = "1" // in seconds
|
||||
defAuthnURL = "localhost:8181"
|
||||
defAuthnTimeout = "1s"
|
||||
|
||||
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"
|
||||
envDB = "MF_THINGS_DB"
|
||||
envDBSSLMode = "MF_THINGS_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_THINGS_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_THINGS_DB_SSL_KEY"
|
||||
@@ -97,8 +97,8 @@ const (
|
||||
envSingleUserEmail = "MF_THINGS_SINGLE_USER_EMAIL"
|
||||
envSingleUserToken = "MF_THINGS_SINGLE_USER_TOKEN"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envAuthURL = "MF_AUTH_URL"
|
||||
envAuthTimeout = "MF_AUTH_TIMEOUT"
|
||||
envAuthnURL = "MF_AUTHN_GRPC_URL"
|
||||
envAuthnTimeout = "MF_AUTHN_GRPC_TIMEOUT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
@@ -120,8 +120,8 @@ type config struct {
|
||||
singleUserEmail string
|
||||
singleUserToken string
|
||||
jaegerURL string
|
||||
authURL string
|
||||
authTimeout time.Duration
|
||||
authnURL string
|
||||
authnTimeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -179,9 +179,9 @@ func loadConfig() config {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envAuthTimeout, defAuthTimeout), 10, 64)
|
||||
authnTimeout, err := time.ParseDuration(mainflux.Env(envAuthnTimeout, defAuthnTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envAuthTimeout, err.Error())
|
||||
log.Fatalf("Invalid %s value: %s", envAuthnTimeout, err.Error())
|
||||
}
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
@@ -189,7 +189,7 @@ func loadConfig() config {
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Name: mainflux.Env(envDBName, defDBName),
|
||||
Name: mainflux.Env(envDB, defDB),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
@@ -215,8 +215,8 @@ func loadConfig() config {
|
||||
singleUserEmail: mainflux.Env(envSingleUserEmail, defSingleUserEmail),
|
||||
singleUserToken: mainflux.Env(envSingleUserToken, defSingleUserToken),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
authURL: mainflux.Env(envAuthURL, defAuthURL),
|
||||
authTimeout: time.Duration(timeout) * time.Second,
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: authnTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ func createAuthClient(cfg config, tracer opentracing.Tracer, logger logger.Logge
|
||||
}
|
||||
|
||||
conn := connectToAuth(cfg, logger)
|
||||
return authapi.NewClient(tracer, conn, cfg.authTimeout), conn.Close
|
||||
return authapi.NewClient(tracer, conn, cfg.authnTimeout), conn.Close
|
||||
}
|
||||
|
||||
func connectToAuth(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
@@ -292,9 +292,9 @@ func connectToAuth(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.authURL, opts...)
|
||||
conn, err := grpc.Dial(cfg.authnURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
logger.Error(fmt.Sprintf("Failed to connect to authn service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -315,9 +315,9 @@ func newService(auth mainflux.AuthNServiceClient, dbTracer opentracing.Tracer, c
|
||||
|
||||
thingCache := rediscache.NewThingCache(cacheClient)
|
||||
thingCache = tracing.ThingCacheMiddleware(cacheTracer, thingCache)
|
||||
idp := uuid.New()
|
||||
up := uuidProvider.New()
|
||||
|
||||
svc := things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, idp)
|
||||
svc := things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, up)
|
||||
svc = rediscache.NewEventStoreMiddleware(svc, esClient)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/mainflux/mainflux"
|
||||
authapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/pkg/messaging"
|
||||
"github.com/mainflux/mainflux/pkg/messaging/nats"
|
||||
uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
|
||||
localusers "github.com/mainflux/mainflux/things/users"
|
||||
"github.com/mainflux/mainflux/twins"
|
||||
"github.com/mainflux/mainflux/twins/api"
|
||||
twapi "github.com/mainflux/mainflux/twins/api/http"
|
||||
twmongodb "github.com/mainflux/mainflux/twins/mongodb"
|
||||
rediscache "github.com/mainflux/mainflux/twins/redis"
|
||||
"github.com/mainflux/mainflux/twins/tracing"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
queue = "twins"
|
||||
|
||||
defLogLevel = "error"
|
||||
defHTTPPort = "8180"
|
||||
defJaegerURL = ""
|
||||
defServerCert = ""
|
||||
defServerKey = ""
|
||||
defDB = "mainflux-twins"
|
||||
defDBHost = "localhost"
|
||||
defDBPort = "27017"
|
||||
defCacheURL = "localhost:6379"
|
||||
defCachePass = ""
|
||||
defCacheDB = "0"
|
||||
defSingleUserEmail = ""
|
||||
defSingleUserToken = ""
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defChannelID = ""
|
||||
defNatsURL = "nats://localhost:4222"
|
||||
defAuthnURL = "localhost:8181"
|
||||
defAuthnTimeout = "1s"
|
||||
|
||||
envLogLevel = "MF_TWINS_LOG_LEVEL"
|
||||
envHTTPPort = "MF_TWINS_HTTP_PORT"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envServerCert = "MF_TWINS_SERVER_CERT"
|
||||
envServerKey = "MF_TWINS_SERVER_KEY"
|
||||
envDB = "MF_TWINS_DB"
|
||||
envDBHost = "MF_TWINS_DB_HOST"
|
||||
envDBPort = "MF_TWINS_DB_PORT"
|
||||
envCacheURL = "MF_TWINS_CACHE_URL"
|
||||
envCachePass = "MF_TWINS_CACHE_PASS"
|
||||
envCacheDB = "MF_TWINS_CACHE_DB"
|
||||
envSingleUserEmail = "MF_TWINS_SINGLE_USER_EMAIL"
|
||||
envSingleUserToken = "MF_TWINS_SINGLE_USER_TOKEN"
|
||||
envClientTLS = "MF_TWINS_CLIENT_TLS"
|
||||
envCACerts = "MF_TWINS_CA_CERTS"
|
||||
envChannelID = "MF_TWINS_CHANNEL_ID"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envAuthnURL = "MF_AUTHN_GRPC_URL"
|
||||
envAuthnTimeout = "MF_AUTHN_GRPC_TIMEOUT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
logLevel string
|
||||
httpPort string
|
||||
jaegerURL string
|
||||
serverCert string
|
||||
serverKey string
|
||||
dbCfg twmongodb.Config
|
||||
cacheURL string
|
||||
cachePass string
|
||||
cacheDB string
|
||||
singleUserEmail string
|
||||
singleUserToken string
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
channelID string
|
||||
natsURL string
|
||||
|
||||
authnURL string
|
||||
authnTimeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
cacheClient := connectToRedis(cfg.cacheURL, cfg.cachePass, cfg.cacheDB, logger)
|
||||
cacheTracer, cacheCloser := initJaeger("twins_cache", cfg.jaegerURL, logger)
|
||||
defer cacheCloser.Close()
|
||||
|
||||
db, err := twmongodb.Connect(cfg.dbCfg, logger)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
dbTracer, dbCloser := initJaeger("twins_db", cfg.jaegerURL, logger)
|
||||
defer dbCloser.Close()
|
||||
|
||||
authTracer, authCloser := initJaeger("auth", cfg.jaegerURL, logger)
|
||||
defer authCloser.Close()
|
||||
auth, _ := createAuthClient(cfg, authTracer, logger)
|
||||
|
||||
pubSub, err := nats.NewPubSub(cfg.natsURL, queue, logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pubSub.Close()
|
||||
|
||||
svc := newService(pubSub, cfg.channelID, auth, dbTracer, db, cacheTracer, cacheClient, logger)
|
||||
|
||||
tracer, closer := initJaeger("twins", cfg.jaegerURL, logger)
|
||||
defer closer.Close()
|
||||
errs := make(chan error, 2)
|
||||
go startHTTPServer(twapi.MakeHandler(tracer, svc), cfg.httpPort, cfg, logger, errs)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("Twins service terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
authnTimeout, err := time.ParseDuration(mainflux.Env(envAuthnTimeout, defAuthnTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envAuthnTimeout, err.Error())
|
||||
}
|
||||
|
||||
dbCfg := twmongodb.Config{
|
||||
Name: mainflux.Env(envDB, defDB),
|
||||
Host: mainflux.Env(envDBHost, defDBHost),
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
}
|
||||
|
||||
return config{
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
serverCert: mainflux.Env(envServerCert, defServerCert),
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
dbCfg: dbCfg,
|
||||
cacheURL: mainflux.Env(envCacheURL, defCacheURL),
|
||||
cachePass: mainflux.Env(envCachePass, defCachePass),
|
||||
cacheDB: mainflux.Env(envCacheDB, defCacheDB),
|
||||
singleUserEmail: mainflux.Env(envSingleUserEmail, defSingleUserEmail),
|
||||
singleUserToken: mainflux.Env(envSingleUserToken, defSingleUserToken),
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
channelID: mainflux.Env(envChannelID, defChannelID),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: authnTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
func initJaeger(svcName, url string, logger logger.Logger) (opentracing.Tracer, io.Closer) {
|
||||
if url == "" {
|
||||
return opentracing.NoopTracer{}, ioutil.NopCloser(nil)
|
||||
}
|
||||
|
||||
tracer, closer, err := jconfig.Configuration{
|
||||
ServiceName: svcName,
|
||||
Sampler: &jconfig.SamplerConfig{
|
||||
Type: "const",
|
||||
Param: 1,
|
||||
},
|
||||
Reporter: &jconfig.ReporterConfig{
|
||||
LocalAgentHostPort: url,
|
||||
LogSpans: true,
|
||||
},
|
||||
}.NewTracer()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to init Jaeger client: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return tracer, closer
|
||||
}
|
||||
|
||||
func createAuthClient(cfg config, tracer opentracing.Tracer, logger logger.Logger) (mainflux.AuthNServiceClient, func() error) {
|
||||
if cfg.singleUserEmail != "" && cfg.singleUserToken != "" {
|
||||
return localusers.NewSingleUserService(cfg.singleUserEmail, cfg.singleUserToken), nil
|
||||
}
|
||||
|
||||
conn := connectToAuth(cfg, logger)
|
||||
return authapi.NewClient(tracer, conn, cfg.authnTimeout), conn.Close
|
||||
}
|
||||
|
||||
func connectToAuth(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.authnURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to authn service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func connectToRedis(cacheURL, cachePass, cacheDB string, logger logger.Logger) *redis.Client {
|
||||
db, err := strconv.Atoi(cacheDB)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to cache: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return redis.NewClient(&redis.Options{
|
||||
Addr: cacheURL,
|
||||
Password: cachePass,
|
||||
DB: db,
|
||||
})
|
||||
}
|
||||
|
||||
func newService(ps messaging.PubSub, chanID string, users mainflux.AuthNServiceClient, dbTracer opentracing.Tracer, db *mongo.Database, cacheTracer opentracing.Tracer, cacheClient *redis.Client, logger logger.Logger) twins.Service {
|
||||
twinRepo := twmongodb.NewTwinRepository(db)
|
||||
twinRepo = tracing.TwinRepositoryMiddleware(dbTracer, twinRepo)
|
||||
|
||||
stateRepo := twmongodb.NewStateRepository(db)
|
||||
stateRepo = tracing.StateRepositoryMiddleware(dbTracer, stateRepo)
|
||||
|
||||
up := uuidProvider.New()
|
||||
twinCache := rediscache.NewTwinCache(cacheClient)
|
||||
twinCache = tracing.TwinCacheMiddleware(cacheTracer, twinCache)
|
||||
|
||||
svc := twins.New(ps, users, twinRepo, twinCache, stateRepo, up, chanID, logger)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "twins",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "twins",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
err := ps.Subscribe(nats.SubjectAllChannels, func(msg messaging.Message) error {
|
||||
if msg.Channel == chanID {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := svc.SaveStates(&msg); err != nil {
|
||||
logger.Error(fmt.Sprintf("State save failed: %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func startHTTPServer(handler http.Handler, port string, cfg config, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
if cfg.serverCert != "" || cfg.serverKey != "" {
|
||||
logger.Info(fmt.Sprintf("Twins service started using https on port %s with cert %s key %s",
|
||||
port, cfg.serverCert, cfg.serverKey))
|
||||
errs <- http.ListenAndServeTLS(p, cfg.serverCert, cfg.serverKey, handler)
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Twins service started using http on port %s", cfg.httpPort))
|
||||
errs <- http.ListenAndServe(p, handler)
|
||||
}
|
||||
+60
-37
@@ -4,6 +4,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
|
||||
"github.com/mainflux/mainflux/internal/email"
|
||||
"github.com/mainflux/mainflux/users"
|
||||
"github.com/mainflux/mainflux/users/bcrypt"
|
||||
"github.com/mainflux/mainflux/users/emailer"
|
||||
"github.com/mainflux/mainflux/users/tracing"
|
||||
"google.golang.org/grpc"
|
||||
@@ -28,7 +30,6 @@ import (
|
||||
authapi "github.com/mainflux/mainflux/authn/api/grpc"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
"github.com/mainflux/mainflux/users/api"
|
||||
"github.com/mainflux/mainflux/users/bcrypt"
|
||||
"github.com/mainflux/mainflux/users/postgres"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
@@ -41,7 +42,7 @@ const (
|
||||
defDBPort = "5432"
|
||||
defDBUser = "mainflux"
|
||||
defDBPass = "mainflux"
|
||||
defDBName = "users"
|
||||
defDB = "users"
|
||||
defDBSSLMode = "disable"
|
||||
defDBSSLCert = ""
|
||||
defDBSSLKey = ""
|
||||
@@ -51,14 +52,6 @@ const (
|
||||
defServerKey = ""
|
||||
defJaegerURL = ""
|
||||
|
||||
defAuthnHTTPPort = "8989"
|
||||
defAuthnGRPCPort = "8181"
|
||||
defAuthnTimeout = "1" // in seconds
|
||||
defAuthnTLS = "false"
|
||||
defAuthnCACerts = ""
|
||||
defAuthnURL = "localhost:8181"
|
||||
|
||||
defEmailLogLevel = "debug"
|
||||
defEmailDriver = "smtp"
|
||||
defEmailHost = "localhost"
|
||||
defEmailPort = "25"
|
||||
@@ -67,15 +60,23 @@ const (
|
||||
defEmailFromAddress = ""
|
||||
defEmailFromName = ""
|
||||
defEmailTemplate = "email.tmpl"
|
||||
defAdminEmail = ""
|
||||
defAdminPassword = ""
|
||||
defAdminGroup = "mainflux"
|
||||
|
||||
defTokenResetEndpoint = "/reset-request" // URL where user lands after click on the reset link from email
|
||||
|
||||
defAuthnTLS = "false"
|
||||
defAuthnCACerts = ""
|
||||
defAuthnURL = "localhost:8181"
|
||||
defAuthnTimeout = "1s"
|
||||
|
||||
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"
|
||||
envDB = "MF_USERS_DB"
|
||||
envDBSSLMode = "MF_USERS_DB_SSL_MODE"
|
||||
envDBSSLCert = "MF_USERS_DB_SSL_CERT"
|
||||
envDBSSLKey = "MF_USERS_DB_SSL_KEY"
|
||||
@@ -85,12 +86,8 @@ const (
|
||||
envServerKey = "MF_USERS_SERVER_KEY"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
|
||||
envAuthnHTTPPort = "MF_AUTHN_HTTP_PORT"
|
||||
envAuthnGRPCPort = "MF_AUTHN_GRPC_PORT"
|
||||
envAuthnTimeout = "MF_AUTHN_TIMEOUT"
|
||||
envAuthnTLS = "MF_AUTHN_CLIENT_TLS"
|
||||
envAuthnCACerts = "MF_AUTHN_CA_CERTS"
|
||||
envAuthnURL = "MF_AUTHN_URL"
|
||||
envAdminEmail = "MF_USERS_ADMIN_EMAIL"
|
||||
envAdminPassword = "MF_USERS_ADMIN_PASSWORD"
|
||||
|
||||
envEmailDriver = "MF_EMAIL_DRIVER"
|
||||
envEmailHost = "MF_EMAIL_HOST"
|
||||
@@ -103,23 +100,28 @@ const (
|
||||
envEmailTemplate = "MF_EMAIL_TEMPLATE"
|
||||
|
||||
envTokenResetEndpoint = "MF_TOKEN_RESET_ENDPOINT"
|
||||
|
||||
envAuthnTLS = "MF_AUTHN_CLIENT_TLS"
|
||||
envAuthnCACerts = "MF_AUTHN_CA_CERTS"
|
||||
envAuthnURL = "MF_AUTHN_GRPC_URL"
|
||||
envAuthnTimeout = "MF_AUTHN_GRPC_TIMEOUT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
logLevel string
|
||||
dbConfig postgres.Config
|
||||
authnHTTPPort string
|
||||
authnGRPCPort string
|
||||
authnTimeout time.Duration
|
||||
authnTLS bool
|
||||
authnCACerts string
|
||||
authnURL string
|
||||
emailConf email.Config
|
||||
httpPort string
|
||||
serverCert string
|
||||
serverKey string
|
||||
jaegerURL string
|
||||
resetURL string
|
||||
authnTLS bool
|
||||
authnCACerts string
|
||||
authnURL string
|
||||
authnTimeout time.Duration
|
||||
adminEmail string
|
||||
adminPassword string
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -129,7 +131,6 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
db := connectToDB(cfg.dbConfig, logger)
|
||||
defer db.Close()
|
||||
|
||||
@@ -163,7 +164,7 @@ func main() {
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envAuthnTimeout, defAuthnTimeout), 10, 64)
|
||||
authnTimeout, err := time.ParseDuration(mainflux.Env(envAuthnTimeout, defAuthnTimeout))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envAuthnTimeout, err.Error())
|
||||
}
|
||||
@@ -178,7 +179,7 @@ func loadConfig() config {
|
||||
Port: mainflux.Env(envDBPort, defDBPort),
|
||||
User: mainflux.Env(envDBUser, defDBUser),
|
||||
Pass: mainflux.Env(envDBPass, defDBPass),
|
||||
Name: mainflux.Env(envDBName, defDBName),
|
||||
Name: mainflux.Env(envDB, defDB),
|
||||
SSLMode: mainflux.Env(envDBSSLMode, defDBSSLMode),
|
||||
SSLCert: mainflux.Env(envDBSSLCert, defDBSSLCert),
|
||||
SSLKey: mainflux.Env(envDBSSLKey, defDBSSLKey),
|
||||
@@ -199,17 +200,18 @@ func loadConfig() config {
|
||||
return config{
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
dbConfig: dbConfig,
|
||||
authnHTTPPort: mainflux.Env(envAuthnHTTPPort, defAuthnHTTPPort),
|
||||
authnGRPCPort: mainflux.Env(envAuthnGRPCPort, defAuthnGRPCPort),
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: time.Duration(timeout) * time.Second,
|
||||
authnTLS: tls,
|
||||
emailConf: emailConf,
|
||||
httpPort: mainflux.Env(envHTTPPort, defHTTPPort),
|
||||
serverCert: mainflux.Env(envServerCert, defServerCert),
|
||||
serverKey: mainflux.Env(envServerKey, defServerKey),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
resetURL: mainflux.Env(envTokenResetEndpoint, defTokenResetEndpoint),
|
||||
authnTLS: tls,
|
||||
authnCACerts: mainflux.Env(envAuthnCACerts, defAuthnCACerts),
|
||||
authnURL: mainflux.Env(envAuthnURL, defAuthnURL),
|
||||
authnTimeout: authnTimeout,
|
||||
adminEmail: mainflux.Env(envAdminEmail, defAdminEmail),
|
||||
adminPassword: mainflux.Env(envAdminPassword, defAdminPassword),
|
||||
}
|
||||
|
||||
}
|
||||
@@ -237,14 +239,12 @@ func initJaeger(svcName, url string, logger logger.Logger) (opentracing.Tracer,
|
||||
|
||||
return tracer, closer
|
||||
}
|
||||
|
||||
func connectToDB(dbConfig postgres.Config, logger logger.Logger) *sqlx.DB {
|
||||
db, err := postgres.Connect(dbConfig)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to postgres: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ func connectToAuthn(cfg config, tracer opentracing.Tracer, logger logger.Logger)
|
||||
|
||||
conn, err := grpc.Dial(cfg.authnURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to users service: %s", err))
|
||||
logger.Error(fmt.Sprintf("Failed to connect to authn service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -275,14 +275,16 @@ func connectToAuthn(cfg config, tracer opentracing.Tracer, logger logger.Logger)
|
||||
|
||||
func newService(db *sqlx.DB, tracer opentracing.Tracer, auth mainflux.AuthNServiceClient, c config, logger logger.Logger) users.Service {
|
||||
database := postgres.NewDatabase(db)
|
||||
repo := tracing.UserRepositoryMiddleware(postgres.New(database), tracer)
|
||||
hasher := bcrypt.New()
|
||||
userRepo := tracing.UserRepositoryMiddleware(postgres.NewUserRepo(database), tracer)
|
||||
groupRepo := tracing.GroupRepositoryMiddleware(postgres.NewGroupRepo(database), tracer)
|
||||
|
||||
emailer, err := emailer.New(c.resetURL, &c.emailConf)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to configure e-mailing util: %s", err.Error()))
|
||||
}
|
||||
|
||||
svc := users.New(repo, hasher, auth, emailer)
|
||||
svc := users.New(userRepo, groupRepo, hasher, auth, emailer)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
@@ -299,10 +301,31 @@ func newService(db *sqlx.DB, tracer opentracing.Tracer, auth mainflux.AuthNServi
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
if err := createAdmin(svc, userRepo, groupRepo, c); err != nil {
|
||||
logger.Error("failed to create admin user: " + err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
func createAdmin(svc users.Service, userRepo users.UserRepository, groupRepo users.GroupRepository, c config) error {
|
||||
user := users.User{
|
||||
Email: c.adminEmail,
|
||||
Password: c.adminPassword,
|
||||
}
|
||||
|
||||
if _, err := userRepo.RetrieveByEmail(context.Background(), user.Email); err == nil {
|
||||
// Exiting if user already exists
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := svc.Register(context.Background(), user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startHTTPServer(tracer opentracing.Tracer, svc users.Service, port string, certFile string, keyFile string, logger logger.Logger, errs chan error) {
|
||||
p := fmt.Sprintf(":%s", port)
|
||||
if certFile != "" || keyFile != "" {
|
||||
|
||||
-198
@@ -1,198 +0,0 @@
|
||||
// Copyright (c) Mainflux
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/logger"
|
||||
thingsapi "github.com/mainflux/mainflux/things/api/auth/grpc"
|
||||
adapter "github.com/mainflux/mainflux/ws"
|
||||
"github.com/mainflux/mainflux/ws/api"
|
||||
"github.com/mainflux/mainflux/ws/nats"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
jconfig "github.com/uber/jaeger-client-go/config"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
defClientTLS = "false"
|
||||
defCACerts = ""
|
||||
defPort = "8180"
|
||||
defLogLevel = "error"
|
||||
defNatsURL = broker.DefaultURL
|
||||
defThingsURL = "localhost:8181"
|
||||
defJaegerURL = ""
|
||||
defThingsTimeout = "1" // in seconds
|
||||
|
||||
envClientTLS = "MF_WS_ADAPTER_CLIENT_TLS"
|
||||
envCACerts = "MF_WS_ADAPTER_CA_CERTS"
|
||||
envPort = "MF_WS_ADAPTER_PORT"
|
||||
envLogLevel = "MF_WS_ADAPTER_LOG_LEVEL"
|
||||
envNatsURL = "MF_NATS_URL"
|
||||
envThingsURL = "MF_THINGS_URL"
|
||||
envJaegerURL = "MF_JAEGER_URL"
|
||||
envThingsTimeout = "MF_WS_ADAPTER_THINGS_TIMEOUT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
clientTLS bool
|
||||
caCerts string
|
||||
thingsURL string
|
||||
natsURL string
|
||||
logLevel string
|
||||
port string
|
||||
jaegerURL string
|
||||
thingsTimeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, err := logger.New(os.Stdout, cfg.logLevel)
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
nc, err := broker.Connect(cfg.natsURL)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to NATS: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
conn := connectToThings(cfg, logger)
|
||||
defer conn.Close()
|
||||
|
||||
thingsTracer, thingsCloser := initJaeger("things", cfg.jaegerURL, logger)
|
||||
defer thingsCloser.Close()
|
||||
|
||||
cc := thingsapi.NewClient(conn, thingsTracer, cfg.thingsTimeout)
|
||||
pubsub := nats.New(nc, logger)
|
||||
svc := newService(pubsub, logger)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.port)
|
||||
logger.Info(fmt.Sprintf("WebSocket adapter service started, exposed port %s", cfg.port))
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, cc, logger))
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
err = <-errs
|
||||
logger.Error(fmt.Sprintf("WebSocket adapter terminated: %s", err))
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
tls, err := strconv.ParseBool(mainflux.Env(envClientTLS, defClientTLS))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid value passed for %s\n", envClientTLS)
|
||||
}
|
||||
|
||||
timeout, err := strconv.ParseInt(mainflux.Env(envThingsTimeout, defThingsTimeout), 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid %s value: %s", envThingsTimeout, err.Error())
|
||||
}
|
||||
|
||||
return config{
|
||||
clientTLS: tls,
|
||||
caCerts: mainflux.Env(envCACerts, defCACerts),
|
||||
thingsURL: mainflux.Env(envThingsURL, defThingsURL),
|
||||
natsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
logLevel: mainflux.Env(envLogLevel, defLogLevel),
|
||||
port: mainflux.Env(envPort, defPort),
|
||||
jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL),
|
||||
thingsTimeout: time.Duration(timeout) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func connectToThings(cfg config, logger logger.Logger) *grpc.ClientConn {
|
||||
var opts []grpc.DialOption
|
||||
if cfg.clientTLS {
|
||||
if cfg.caCerts != "" {
|
||||
tpc, err := credentials.NewClientTLSFromFile(cfg.caCerts, "")
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to load certs: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(tpc))
|
||||
}
|
||||
} else {
|
||||
logger.Info("gRPC communication is not encrypted")
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(cfg.thingsURL, opts...)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to connect to things service: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
func initJaeger(svcName, url string, logger logger.Logger) (opentracing.Tracer, io.Closer) {
|
||||
if url == "" {
|
||||
return opentracing.NoopTracer{}, ioutil.NopCloser(nil)
|
||||
}
|
||||
|
||||
tracer, closer, err := jconfig.Configuration{
|
||||
ServiceName: svcName,
|
||||
Sampler: &jconfig.SamplerConfig{
|
||||
Type: "const",
|
||||
Param: 1,
|
||||
},
|
||||
Reporter: &jconfig.ReporterConfig{
|
||||
LocalAgentHostPort: url,
|
||||
LogSpans: true,
|
||||
},
|
||||
}.NewTracer()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to init Jaeger client: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return tracer, closer
|
||||
}
|
||||
|
||||
func newService(pubsub adapter.Service, logger logger.Logger) adapter.Service {
|
||||
svc := adapter.New(pubsub)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "ws_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "ws_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
return svc
|
||||
}
|
||||
+14
-5
@@ -13,13 +13,13 @@ default values.
|
||||
|--------------------------------|--------------------------------------------------------|-----------------------|
|
||||
| MF_COAP_ADAPTER_PORT | Service listening port | 5683 |
|
||||
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
|
||||
| MF_THINGS_URL | Things service URL | localhost:8181 |
|
||||
| MF_COAP_ADAPTER_LOG_LEVEL | Service log level | error |
|
||||
| MF_COAP_ADAPTER_CLIENT_TLS | Flag that indicates if TLS should be turned on | false |
|
||||
| MF_COAP_ADAPTER_CA_CERTS | Path to trusted CAs in PEM format | |
|
||||
| MF_COAP_ADAPTER_PING_PERIOD | Hours between 1 and 24 to ping client with ACK message | 12 |
|
||||
| MF_JAEGER_URL | Jaeger server URL | localhost:6831 |
|
||||
| MF_COAP_ADAPTER_THINGS_TIMEOUT | Things gRPC request timeout in seconds | 1 |
|
||||
| MF_THINGS_AUTH_GRPC_URL | Things service Auth gRPC URL | localhost:8181 |
|
||||
| MF_THINGS_AUTH_GRPC_TIMEOUT | Things service Auth gRPC request timeout in seconds | 1s |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -37,13 +37,13 @@ services:
|
||||
environment:
|
||||
MF_COAP_ADAPTER_PORT: [Service HTTP port]
|
||||
MF_NATS_URL: [NATS instance URL]
|
||||
MF_THINGS_URL: [Things service URL]
|
||||
MF_COAP_ADAPTER_LOG_LEVEL: [Service log level]
|
||||
MF_COAP_ADAPTER_CLIENT_TLS: [Flag that indicates if TLS should be turned on]
|
||||
MF_COAP_ADAPTER_CA_CERTS: [Path to trusted CAs in PEM format]
|
||||
MF_COAP_ADAPTER_PING_PERIOD: [Hours between 1 and 24 to ping client with ACK message]
|
||||
MF_JAEGER_URL: [Jaeger server URL]
|
||||
MF_COAP_ADAPTER_THINGS_TIMEOUT: [Things gRPC request timeout in seconds]
|
||||
MF_THINGS_AUTH_GRPC_URL: [Things service Auth gRPC URL]
|
||||
MF_THINGS_AUTH_GRPC_TIMEOUT: [Things service Auth gRPC request timeout in seconds]
|
||||
```
|
||||
|
||||
Running this service outside of container requires working instance of the NATS service.
|
||||
@@ -62,7 +62,16 @@ make coap
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_THINGS_URL=[Things service URL] MF_NATS_URL=[NATS instance URL] MF_COAP_ADAPTER_PORT=[Service HTTP port] MF_COAP_ADAPTER_LOG_LEVEL=[Service log level] MF_COAP_ADAPTER_CLIENT_TLS=[Flag that indicates if TLS should be turned on] MF_COAP_ADAPTER_CA_CERTS=[Path to trusted CAs in PEM format] MF_COAP_ADAPTER_PING_PERIOD: [Hours between 1 and 24 to ping client with ACK message] MF_JAEGER_URL=[Jaeger server URL] MF_COAP_ADAPTER_THINGS_TIMEOUT=[Things gRPC request timeout in seconds] $GOBIN/mainflux-coap
|
||||
MF_NATS_URL=[NATS instance URL] \
|
||||
MF_COAP_ADAPTER_PORT=[Service HTTP port] \
|
||||
MF_COAP_ADAPTER_LOG_LEVEL=[Service log level] \
|
||||
MF_COAP_ADAPTER_CLIENT_TLS=[Flag that indicates if TLS should be turned on] \
|
||||
MF_COAP_ADAPTER_CA_CERTS=[Path to trusted CAs in PEM format] \
|
||||
MF_COAP_ADAPTER_PING_PERIOD: [Hours between 1 and 24 to ping client with ACK message] \
|
||||
MF_JAEGER_URL=[Jaeger server URL] \
|
||||
MF_THINGS_AUTH_GRPC_URL=[Things service Auth gRPC URL] \
|
||||
MF_THINGS_AUTH_GRPC_TIMEOUT=[Things service Auth gRPC request timeout in seconds] \
|
||||
$GOBIN/mainflux-coap
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user