Ver código fonte

initial commit

willie 3 anos atrás
pai
commit
23dd8aada3
51 arquivos alterados com 5732 adições e 72 exclusões
  1. 201 72
      LICENSE
  2. 22 0
      MessageService/.vscode/launch.json
  3. 5 0
      MessageService/.vscode/settings.json
  4. 39 0
      MessageService/Makefile
  5. 20 0
      MessageService/README.md
  6. 75 0
      MessageService/api/errorapi.go
  7. 70 0
      MessageService/api/filesapi.go
  8. 313 0
      MessageService/api/modelapi.go
  9. 33 0
      MessageService/api/responses.go
  10. 24 0
      MessageService/api/rolehandler.go
  11. 98 0
      MessageService/api/sysapihandler.go
  12. 47 0
      MessageService/api/tasksapi.go
  13. 160 0
      MessageService/api/userapi.go
  14. 70 0
      MessageService/cmd/health_test.go
  15. 498 0
      MessageService/cmd/service.go
  16. 78 0
      MessageService/cmd/service_test.go
  17. 114 0
      MessageService/cmd/userapi_test.go
  18. 73 0
      MessageService/config/loader.go
  19. 11 0
      MessageService/config/secret.go
  20. 53 0
      MessageService/configs/backends/schematic.yaml
  21. 192 0
      MessageService/configs/backends/sensor.yaml
  22. 39 0
      MessageService/configs/backends/simple.yaml
  23. 3 0
      MessageService/configs/secret.yaml
  24. 34 0
      MessageService/configs/service.yaml
  25. 30 0
      MessageService/configs/serviceLocal.yaml
  26. 36 0
      MessageService/configs/service_home.yaml
  27. 15 0
      MessageService/dao/errors.go
  28. 263 0
      MessageService/dao/idm.go
  29. 718 0
      MessageService/dao/mongodao.go
  30. 57 0
      MessageService/dao/storageDao.go
  31. 16 0
      MessageService/devdata/mongo.txt
  32. 44 0
      MessageService/devdata/notice.txt
  33. 33 0
      MessageService/go.mod
  34. 568 0
      MessageService/go.sum
  35. 109 0
      MessageService/health/health.go
  36. 140 0
      MessageService/internal/crypt/generatecert.go
  37. 31 0
      MessageService/internal/crypt/salt.go
  38. 45 0
      MessageService/internal/slicesutils/slicesutils.go
  39. 58 0
      MessageService/internal/slicesutils/slicesutils_test.go
  40. 125 0
      MessageService/logging/logging.go
  41. 14 0
      MessageService/model/model.go
  42. 14 0
      MessageService/model/mqtt.go
  43. 27 0
      MessageService/model/route.go
  44. 45 0
      MessageService/model/task.go
  45. 17 0
      MessageService/model/user.go
  46. 34 0
      MessageService/scratch.Dockerfile
  47. 326 0
      MessageService/worker/model.go
  48. 423 0
      MessageService/worker/mqtt.go
  49. 80 0
      MessageService/worker/rules.go
  50. 185 0
      MessageService/worker/rules_test.go
  51. 7 0
      msg-srv.code-workspace

+ 201 - 72
LICENSE

@@ -1,72 +1,201 @@
-Apache License 
-Version 2.0, January 2004 
-http://www.apache.org/licenses/
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-1. Definitions.
-
-"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
-
-"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
-
-"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
-"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
-
-"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
-
-"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
-
-"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
-
-"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
-
-"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
-
-"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
-
-2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
-
-3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
-
-4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
-
-(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
-
-(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
-
-(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
-
-(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
-
-You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
-
-5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
-
-6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
-
-7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
-
-8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
-
-9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
-
-END OF TERMS AND CONDITIONS
-
-APPENDIX: How to apply the Apache License to your work.
-
-To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
-
-Copyright [yyyy] [name of copyright owner]
-
-Licensed under the Apache License, Version 2.0 (the "License"); 
-you may not use this file except in compliance with the License. 
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software 
-distributed under the License is distributed on an "AS IS" BASIS, 
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
-See the License for the specific language governing permissions and 
-limitations under the License.
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 22 - 0
MessageService/.vscode/launch.json

@@ -0,0 +1,22 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+    
+        {
+            "name": "Launch",
+            "type": "go",
+            "request": "launch",
+            "mode": "auto",
+            "program": "${workspaceFolder}/cmd/service.go",
+            "cwd": "${workspaceFolder}",
+            "env": {},
+            "args": [
+                "-c",
+                "configs/service_home.yaml",
+            ]
+        }
+    ]
+}

+ 5 - 0
MessageService/.vscode/settings.json

@@ -0,0 +1,5 @@
+{
+    "go.gotoSymbol.includeImports": true,
+    "go.inferGopath": false,
+    "go.testFlags": [ "-v"]
+}

+ 39 - 0
MessageService/Makefile

@@ -0,0 +1,39 @@
+## Taken from http://www.codershaven.com/multi-platform-makefile-for-go/
+## Could be a good possibility if more than one platform must be supported
+## It also use the git version as version for the binary.
+EXECUTABLE=bin/autorestsrv
+WINDOWS=$(EXECUTABLE)_windows_amd64.exe
+LINUX=$(EXECUTABLE)_linux_amd64
+DARWIN=$(EXECUTABLE)_darwin_amd64
+VERSION=$(shell git describe --tags --always --long --dirty)
+LDFLAGS=-s -w -X main.version=$(VERSION)
+
+windows: $(WINDOWS) ## Build for Windows
+
+linux: $(LINUX) ## Build for Linux
+
+darwin: $(DARWIN) ## Build for Darwin (macOS)
+
+$(WINDOWS):
+	env GOOS=windows GOARCH=amd64 go build -i -v -o $(WINDOWS) -ldflags="$(LDFLAGS)"  
+
+$(LINUX):
+	env GOOS=linux GOARCH=amd64 go build -i -v -o $(LINUX) -ldflags="$(LDFLAGS)"
+
+$(DARWIN):
+	env GOOS=darwin GOARCH=amd64 go build -i -v -o $(DARWIN) -ldflags="$(LDFLAGS)"
+
+
+build: windows linux darwin ## Build binaries
+	@echo version: $(VERSION)
+
+all: test build ## Build and run tests
+
+test: ## Run unit tests
+	go test
+
+clean: ## Remove previous build
+	rm -f $(WINDOWS) $(LINUX) $(DARWIN)
+
+help: ## Display available commands
+	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

+ 20 - 0
MessageService/README.md

@@ -0,0 +1,20 @@
+# Auto Rest Service #
+
+This is the service for the auto rest IoT backend system
+
+
+
+For german:
+
+Liste der Features
+
+- einheitliches REST Interface für alle Modelle
+- einfache Definition des Backends, nur die notwendigen Dinge müssen definiert werden
+- Einfaches User/Rollen Konzept 
+- Einfaches Query Interface
+- automatische Anbindung an MQTT Server als Datenquelle
+- Admin Interface zum Anlegen von Usern, Rollen, Backend
+- Admin Interface zur Änderung von Indexen
+- Attributvalidierung
+- neben der eigenen Datenspeicherung verschiedene andere Datensenken, wie MQTT, EMail, 
+

+ 75 - 0
MessageService/api/errorapi.go

@@ -0,0 +1,75 @@
+package api
+
+import (
+	"net/http"
+
+	"github.com/go-chi/render"
+)
+
+// ErrResponse renderer type for handling all sorts of errors.
+type ErrResponse struct {
+	Err            error `json:"-"` // low-level runtime error
+	HTTPStatusCode int   `json:"-"` // http response status code
+
+	StatusText string `json:"status"`          // user-level status message
+	AppCode    int64  `json:"code,omitempty"`  // application-specific error code
+	ErrorText  string `json:"error,omitempty"` // application-level error message, for debugging
+}
+
+//Render render the error automaticaly to the response
+func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
+	render.Status(r, e.HTTPStatusCode)
+	return nil
+}
+
+//ErrInvalidRequest creates a new Invalid request error response
+func ErrInvalidRequest(err error) render.Renderer {
+	return &ErrResponse{
+		Err:            err,
+		HTTPStatusCode: http.StatusBadRequest,
+		StatusText:     "Invalid request.",
+		ErrorText:      err.Error(),
+	}
+}
+
+//ErrRender creates a wrapper for any error when an output can not be rendered
+func ErrRender(err error) render.Renderer {
+	return &ErrResponse{
+		Err:            err,
+		HTTPStatusCode: http.StatusUnprocessableEntity,
+		StatusText:     "Error rendering response.",
+		ErrorText:      err.Error(),
+	}
+}
+
+//ErrInternalServer render a internal server error, with another error as source
+func ErrInternalServer(err error) render.Renderer {
+	return &ErrResponse{
+		Err:            err,
+		HTTPStatusCode: http.StatusInternalServerError,
+		StatusText:     "Internal server error.",
+		ErrorText:      err.Error(),
+	}
+}
+
+//ErrValidationError error on validating objects
+func ErrValidationError(err error) render.Renderer {
+	return &ErrResponse{
+		Err:            err,
+		HTTPStatusCode: http.StatusBadRequest,
+		StatusText:     "Validation error.",
+		ErrorText:      err.Error(),
+	}
+}
+
+//ErrNotFound requested resource was not found
+var ErrNotFound = &ErrResponse{HTTPStatusCode: http.StatusNotFound, StatusText: "Resource not found."}
+
+//ErrNotImplemted this feature/function/methode is not implemented yet
+var ErrNotImplemted = &ErrResponse{HTTPStatusCode: http.StatusNotImplemented, StatusText: "Not im plemented yet."}
+
+//ErrUniqueIndexError index violation error
+var ErrUniqueIndexError = &ErrResponse{HTTPStatusCode: http.StatusBadRequest, StatusText: "Unique index violation."}
+
+//ErrForbidden not enough rights for doing this
+var ErrForbidden = &ErrResponse{HTTPStatusCode: http.StatusForbidden, StatusText: "endpoint not permitted."}

+ 70 - 0
MessageService/api/filesapi.go

@@ -0,0 +1,70 @@
+package api
+
+import (
+	"bufio"
+	"fmt"
+	"net/http"
+
+	"github.com/go-chi/chi"
+	"github.com/go-chi/render"
+	"github.com/willie68/AutoRestIoT/dao"
+)
+
+//FilesRoutes getting all routes for the config endpoint
+func FilesRoutes() *chi.Mux {
+	router := chi.NewRouter()
+	router.With(RoleCheck([]string{"edit", "read"})).Get("/{bename}/{fileId}", GetFileHandler)
+	router.With(RoleCheck([]string{"edit"})).Post("/{bename}/", PostFileEndpoint)
+	return router
+}
+
+// GetFileHandler get a file
+func GetFileHandler(response http.ResponseWriter, request *http.Request) {
+	backend := chi.URLParam(request, "bename")
+	fileID := chi.URLParam(request, "fileId")
+	log.Infof("GET: path: %s, be: %s, fileID: %s", request.URL.Path, backend, fileID)
+	filename, err := dao.GetStorage().GetFilename(backend, fileID)
+	if filename == "" {
+		render.Render(response, request, ErrNotFound)
+		return
+	}
+	response.Header().Add("Content-disposition", "attachment; filename=\""+filename+"\"")
+	err = dao.GetStorage().GetFile(backend, fileID, response)
+	if err != nil {
+		render.Render(response, request, ErrInvalidRequest(err))
+		return
+	}
+}
+
+//PostFileEndpoint create a new file, return the id
+func PostFileEndpoint(response http.ResponseWriter, request *http.Request) {
+	backend := chi.URLParam(request, "bename")
+	log.Infof("POST: path: %s, be: %s", request.URL.Path, backend)
+	request.ParseForm()
+	f, fileHeader, err := request.FormFile("file")
+	if err != nil {
+		render.Render(response, request, ErrInvalidRequest(err))
+		return
+	}
+
+	//mimeType := fileHeader.Header.Get("Content-type")
+	filename := fileHeader.Filename
+	reader := bufio.NewReader(f)
+
+	fileid, err := dao.GetStorage().AddFile(backend, filename, reader)
+	if err != nil {
+		render.Render(response, request, ErrInvalidRequest(err))
+		return
+	}
+	log.Infof("fileid: %s", fileid)
+
+	location := fmt.Sprintf("/api/v1/files/%s/%s", backend, fileid)
+	response.Header().Add("Location", location)
+	render.Status(request, http.StatusCreated)
+
+	m := make(map[string]interface{})
+	m["fileid"] = fileid
+	m["filename"] = filename
+
+	render.JSON(response, request, m)
+}

+ 313 - 0
MessageService/api/modelapi.go

@@ -0,0 +1,313 @@
+package api
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/go-chi/chi"
+	"github.com/go-chi/render"
+	"github.com/willie68/AutoRestIoT/dao"
+	"github.com/willie68/AutoRestIoT/internal"
+	"github.com/willie68/AutoRestIoT/model"
+	"github.com/willie68/AutoRestIoT/worker"
+)
+
+//DeleteRefHeader header key for the delete reference options
+const DeleteRefHeader = "X-mcs-deleteref"
+
+/*
+ModelRoutes getting all routes for the config endpoint
+*/
+func ModelRoutes() *chi.Mux {
+	router := chi.NewRouter()
+	router.With(RoleCheck([]string{"edit"})).Post("/{bename}/{model}/", PostModelEndpoint)
+	//TODO insert bulk import api
+	router.With(RoleCheck([]string{"edit", "read"})).Get("/{bename}/{model}/count", GetModelCountEndpoint)
+	router.With(RoleCheck([]string{"edit", "read"})).With(Paginate).Get("/{bename}/{model}/", GetManyModelsEndpoint)
+	router.With(RoleCheck([]string{"edit", "read"})).Get("/{bename}/{model}/{modelid}", GetModelEndpoint)
+	router.With(RoleCheck([]string{"edit"})).Put("/{bename}/{model}/{modelid}", PutModelEndpoint)
+	router.With(RoleCheck([]string{"edit"})).Delete("/{bename}/{model}/{modelid}", DeleteModelEndpoint)
+	return router
+}
+
+//PostModelEndpoint , this method will always return 201
+func PostModelEndpoint(response http.ResponseWriter, request *http.Request) {
+	backend := chi.URLParam(request, "bename")
+	mymodel := chi.URLParam(request, "model")
+	route := model.Route{
+		Backend: backend,
+		Model:   mymodel,
+	}
+	route = enrichRouteInformation(request, route)
+	data := &model.JSONMap{}
+
+	if err := render.Decode(request, data); err != nil {
+		render.Render(response, request, ErrInvalidRequest(err))
+		return
+	}
+
+	bemodel := *data
+
+	log.Infof("POST: path: %s, route: %s \n", request.URL.Path, route.String())
+	validModel, err := worker.Validate(route, bemodel)
+	if err != nil {
+		if pe, ok := err.(worker.ErrValidationError); ok {
+			render.Render(response, request, ErrInvalidRequest(pe))
+			return
+		}
+		if err == dao.ErrNotImplemented {
+			render.Render(response, request, ErrNotImplemted)
+			return
+		}
+		if err == worker.ErrBackendNotFound || err == worker.ErrBackendModelNotFound {
+			render.Render(response, request, ErrNotFound)
+			return
+		}
+		render.Render(response, request, ErrInternalServer(err))
+		return
+	}
+	if validModel == nil {
+		render.Render(response, request, ErrInvalidRequest(errors.New("data model not valid")))
+		return
+	}
+	validModel, err = worker.Store(route, validModel)
+	if err != nil {
+		if err == dao.ErrNotImplemented {
+			render.Render(response, request, ErrNotImplemted)
+			return
+		}
+		if err == dao.ErrUniqueIndexError {
+			render.Render(response, request, ErrUniqueIndexError)
+			return
+		}
+		render.Render(response, request, ErrInternalServer(err))
+		return
+	}
+
+	route.Identity = validModel[internal.AttributeID].(string)
+
+	buildLocationHeader(response, request, route)
+
+	render.Status(request, http.StatusCreated)
+	render.JSON(response, request, validModel)
+}
+
+//GetManyModelsEndpoint searching for a list of model instances
+func GetManyModelsEndpoint(response http.ResponseWriter, request *http.Request) {
+	offset := request.Context().Value(contextKeyOffset)
+	limit := request.Context().Value(contextKeyLimit)
+	log.Infof("GET many: offset: %d, limit: %d", offset, limit)
+
+	backend := chi.URLParam(request, "bename")
+	mymodel := chi.URLParam(request, "model")
+	route := model.Route{
+		Backend: backend,
+		Model:   mymodel,
+	}
+	route = enrichRouteInformation(request, route)
+	log.Infof("GET many: path: %s, route: %s", request.URL.Path, route.String())
+
+	query := request.URL.Query().Get("query")
+
+	log.Infof("query: %s, offset: %d, limit: %d", query, offset, limit)
+	//owner, _, _ := request.BasicAuth()
+
+	n, models, err := worker.Query(route, query, offset.(int), limit.(int))
+	if err != nil {
+		if err == dao.ErrNotImplemented {
+			render.Render(response, request, ErrNotImplemted)
+			return
+		}
+		render.Render(response, request, ErrInternalServer(err))
+		return
+	}
+
+	m := make(map[string]interface{})
+	m["data"] = models
+	m["found"] = n
+	m["count"] = len(models)
+	m["query"] = query
+	m["offset"] = offset
+	m["limit"] = limit
+
+	render.JSON(response, request, m)
+}
+
+//GetModelCountEndpoint getting a model with an identifier
+func GetModelCountEndpoint(response http.ResponseWriter, request *http.Request) {
+	backend := chi.URLParam(request, "bename")
+	mymodel := chi.URLParam(request, "model")
+	route := model.Route{
+		Backend: backend,
+		Model:   mymodel,
+	}
+	route = enrichRouteInformation(request, route)
+	log.Infof("GET count: path: %s, route: %s", request.URL.Path, route.String())
+
+	modelCount, err := worker.GetCount(route)
+	if err != nil {
+		if err == dao.ErrNotImplemented {
+			render.Render(response, request, ErrNotImplemted)
+			return
+		}
+		if err == dao.ErrNoDocument {
+			render.Render(response, request, ErrNotFound)
+			return
+		}
+		render.Render(response, request, ErrInternalServer(err))
+		return
+	}
+	m := make(map[string]interface{})
+	m["found"] = modelCount
+
+	render.JSON(response, request, m)
+}
+
+//GetModelEndpoint getting a model with an identifier
+func GetModelEndpoint(response http.ResponseWriter, request *http.Request) {
+	backend := chi.URLParam(request, "bename")
+	mymodel := chi.URLParam(request, "model")
+	modelid := chi.URLParam(request, "modelid")
+	route := model.Route{
+		Backend:  backend,
+		Model:    mymodel,
+		Identity: modelid,
+	}
+	route = enrichRouteInformation(request, route)
+	log.Infof("GET: path: %s, route: %s", request.URL.Path, route.String())
+
+	model, err := worker.Get(route)
+	if err != nil {
+		if err == dao.ErrNotImplemented {
+			render.Render(response, request, ErrNotImplemted)
+			return
+		}
+		if err == dao.ErrNoDocument {
+			render.Render(response, request, ErrNotFound)
+			return
+		}
+		render.Render(response, request, ErrInternalServer(err))
+		return
+	}
+	render.JSON(response, request, model)
+}
+
+//PutModelEndpoint change a model
+func PutModelEndpoint(response http.ResponseWriter, request *http.Request) {
+	backend := chi.URLParam(request, "bename")
+	mymodel := chi.URLParam(request, "model")
+	modelid := chi.URLParam(request, "modelid")
+	route := model.Route{
+		Backend:  backend,
+		Model:    mymodel,
+		Identity: modelid,
+	}
+	route = enrichRouteInformation(request, route)
+	log.Infof("PUT: path: %s, route: %s", request.URL.Path, route.String())
+	data := &model.JSONMap{}
+
+	if err := render.Decode(request, data); err != nil {
+		render.Render(response, request, ErrInvalidRequest(err))
+		return
+	}
+
+	bemodel := *data
+	validModel, err := worker.Validate(route, bemodel)
+	if err != nil {
+		if err == dao.ErrNotImplemented {
+			render.Render(response, request, ErrNotImplemted)
+			return
+		}
+		render.Render(response, request, ErrInternalServer(err))
+		return
+	}
+	if validModel == nil {
+		render.Render(response, request, ErrInvalidRequest(errors.New("data model not valid")))
+		return
+	}
+
+	validModel, err = worker.Update(route, validModel)
+	if err != nil {
+		if err == dao.ErrNoDocument {
+			render.Render(response, request, ErrNotFound)
+			return
+		}
+		if err == dao.ErrNotImplemented {
+			render.Render(response, request, ErrNotImplemted)
+			return
+		}
+		render.Render(response, request, ErrInternalServer(err))
+		return
+	}
+
+	route.Identity = validModel[internal.AttributeID].(string)
+
+	buildLocationHeader(response, request, route)
+
+	render.Status(request, http.StatusCreated)
+	render.JSON(response, request, validModel)
+}
+
+//DeleteModelEndpoint deleting a model
+func DeleteModelEndpoint(response http.ResponseWriter, request *http.Request) {
+	backend := chi.URLParam(request, "bename")
+	mymodel := chi.URLParam(request, "model")
+	modelid := chi.URLParam(request, "modelid")
+	route := model.Route{
+		Backend:  backend,
+		Model:    mymodel,
+		Identity: modelid,
+	}
+	route = enrichRouteInformation(request, route)
+
+	deleteRef := isDeleteRef(request)
+	log.Infof("DELETE: path: %s,  route: %s, delRef: %t", request.URL.Path, route.String(), deleteRef)
+
+	err := worker.Delete(route, deleteRef)
+	if err != nil {
+		if err == dao.ErrNoDocument {
+			render.Render(response, request, ErrNotFound)
+			return
+		}
+		if err == dao.ErrNotImplemented {
+			render.Render(response, request, ErrNotImplemted)
+			return
+		}
+		render.Render(response, request, ErrInternalServer(err))
+		return
+	}
+	MsgResponse(response, http.StatusOK, "model deleted.")
+}
+
+func isDeleteRef(request *http.Request) bool {
+	deleteRef := true
+	if request.Header.Get(DeleteRefHeader) != "" {
+		b, err := strconv.ParseBool(request.Header.Get(DeleteRefHeader))
+		if err == nil {
+			deleteRef = b
+		}
+	}
+	return deleteRef
+}
+
+func enrichRouteInformation(request *http.Request, route model.Route) model.Route {
+	if request.Header.Get(APIKeyHeader) != "" {
+		route.Apikey = request.Header.Get(APIKeyHeader)
+	}
+	if request.Header.Get(SystemHeader) != "" {
+		route.SystemID = request.Header.Get(SystemHeader)
+	}
+	username, _, _ := request.BasicAuth()
+	if username != "" {
+		route.Username = username
+	}
+	return route
+}
+
+func buildLocationHeader(response http.ResponseWriter, request *http.Request, route model.Route) {
+
+	loc := fmt.Sprintf("%s/%s", request.URL.Path, route.Identity)
+	response.Header().Add("Location", loc)
+}

+ 33 - 0
MessageService/api/responses.go

@@ -0,0 +1,33 @@
+package api
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+)
+
+//SimpleResponseMessage a simple message as response
+type SimpleResponseMessage struct {
+	Message string `json:"message"`
+	Code    int    `json:"code"`
+}
+
+//MsgResponse writes a response with a message as json
+func MsgResponse(w http.ResponseWriter, code int, message string) {
+	m := SimpleResponseMessage{
+		Message: message,
+		Code:    code,
+	}
+
+	buf := &bytes.Buffer{}
+	enc := json.NewEncoder(buf)
+	enc.SetEscapeHTML(true)
+	if err := enc.Encode(m); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+	w.WriteHeader(code)
+	w.Write(buf.Bytes())
+}

+ 24 - 0
MessageService/api/rolehandler.go

@@ -0,0 +1,24 @@
+package api
+
+import (
+	"net/http"
+
+	"github.com/go-chi/render"
+	"github.com/willie68/AutoRestIoT/dao"
+)
+
+//RoleCheck implements a simple middleware handler for adding a role check to a route.
+func RoleCheck(allowedRoles []string) func(next http.Handler) http.Handler {
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
+			user, _, _ := request.BasicAuth()
+			idm := dao.GetIDM()
+			ok := idm.UserInRoles(user, allowedRoles)
+			if !ok {
+				render.Render(response, request, ErrForbidden)
+				return
+			}
+			next.ServeHTTP(response, request)
+		})
+	}
+}

+ 98 - 0
MessageService/api/sysapihandler.go

@@ -0,0 +1,98 @@
+package api
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"github.com/go-chi/render"
+)
+
+//APIKeyHeader in this header thr right api key should be inserted
+const APIKeyHeader = "X-mcs-apikey"
+
+//SystemHeader in this header thr right system should be inserted
+const SystemHeader = "X-mcs-system"
+
+//SysAPIKey defining a handler for checking system id and api key
+type SysAPIKey struct {
+	SystemID string
+	Apikey   string
+}
+
+//NewSysAPIHandler creates a new SysApikeyHandler
+func NewSysAPIHandler(systemID string, apikey string) *SysAPIKey {
+	c := &SysAPIKey{
+		SystemID: systemID,
+		Apikey:   apikey,
+	}
+	return c
+}
+
+//Handler the handler checks systemid and apikey headers
+func (s *SysAPIKey) Handler(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
+		path := strings.TrimSuffix(request.URL.Path, "/")
+		if !strings.HasPrefix(path, "/health") && !strings.HasPrefix(path, "/files") {
+			if s.SystemID != request.Header.Get(SystemHeader) {
+				render.Render(response, request, ErrInvalidRequest(errors.New("either system id or apikey not correct")))
+				return
+			}
+			if s.Apikey != strings.ToLower(request.Header.Get(APIKeyHeader)) {
+				render.Render(response, request, ErrInvalidRequest(errors.New("either system id or apikey not correct")))
+				return
+			}
+		}
+		next.ServeHTTP(response, request)
+	})
+
+}
+
+//AddHeader adding gefault header for system and apikey
+func AddHeader(response http.ResponseWriter, apikey string, system string) {
+	response.Header().Add(APIKeyHeader, apikey)
+	response.Header().Add(SystemHeader, system)
+}
+
+var (
+	contextKeyOffset = contextKey("offset")
+	contextKeyLimit  = contextKey("limit")
+)
+
+//Paginate is a middleware logic for populating the context with offset and limit values
+func Paginate(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
+		ctx := request.Context()
+		offsetStr := request.URL.Query().Get("offset")
+		limitStr := request.URL.Query().Get("limit")
+		if offsetStr != "" {
+			offset, err := strconv.Atoi(offsetStr)
+			if err != nil {
+				render.Render(response, request, ErrInvalidRequest(err))
+				return
+			}
+			ctx = context.WithValue(ctx, contextKeyOffset, offset)
+		} else {
+			ctx = context.WithValue(ctx, contextKeyOffset, 0)
+		}
+		if limitStr != "" {
+			limit, err := strconv.Atoi(limitStr)
+			if err != nil {
+				render.Render(response, request, ErrInvalidRequest(err))
+				return
+			}
+			ctx = context.WithValue(ctx, contextKeyLimit, limit)
+		} else {
+			ctx = context.WithValue(ctx, contextKeyLimit, 0)
+		}
+		next.ServeHTTP(response, request.WithContext(ctx))
+	})
+}
+
+type contextKey string
+
+func (c contextKey) String() string {
+	return "api" + string(c)
+}

+ 47 - 0
MessageService/api/tasksapi.go

@@ -0,0 +1,47 @@
+package api
+
+import (
+	"net/http"
+
+	"github.com/go-chi/chi"
+	"github.com/go-chi/render"
+	"github.com/willie68/AutoRestIoT/dao"
+	"github.com/willie68/AutoRestIoT/worker"
+)
+
+//AdminRoutes getting all routes for the config endpoint
+func TasksRoutes() *chi.Mux {
+	router := chi.NewRouter()
+	router.With(RoleCheck([]string{"admin"})).Get("/", GetAdminTasksHandler)
+	return router
+}
+
+// GetAdminTasksHandler getting server info
+func GetAdminTasksHandler(response http.ResponseWriter, request *http.Request) {
+	log.Infof("GET: path: %s", request.URL.Path)
+	route := worker.GetTaskRoute()
+
+	log.Infof("GET many: path: %s, route: %s", request.URL.Path, route.String())
+
+	query := ""
+
+	n, models, err := worker.Query(route, query, 0, 0)
+	if err != nil {
+		if err == dao.ErrNotImplemented {
+			render.Render(response, request, ErrNotImplemted)
+			return
+		}
+		render.Render(response, request, ErrInternalServer(err))
+		return
+	}
+
+	m := make(map[string]interface{})
+	m["data"] = models
+	m["found"] = n
+	m["count"] = len(models)
+	m["query"] = query
+	m["offset"] = 0
+	m["limit"] = 0
+
+	render.JSON(response, request, m)
+}

+ 160 - 0
MessageService/api/userapi.go

@@ -0,0 +1,160 @@
+package api
+
+import (
+	"errors"
+	"net/http"
+
+	"github.com/go-chi/chi"
+	"github.com/go-chi/render"
+	"github.com/willie68/AutoRestIoT/dao"
+	"github.com/willie68/AutoRestIoT/model"
+)
+
+// UsersRoutes routes to the user interface
+func UsersRoutes() *chi.Mux {
+	router := chi.NewRouter()
+	router.Get("/me", GetMeEndpoint)
+	router.With(RoleCheck([]string{"admin"})).Get("/", GetUsersEndpoint)
+	router.With(RoleCheck([]string{"admin"})).Post("/", PostUserEndpoint)
+	router.With(RoleCheck([]string{"admin"})).Get("/{username}", GetUserEndpoint)
+	router.With(RoleCheck([]string{"admin"})).Put("/{username}", PutUserEndpoint)
+	router.With(RoleCheck([]string{"admin"})).Delete("/{username}", DeleteUserEndpoint)
+	return router
+}
+
+//GetMeEndpoint getting all user infos
+func GetMeEndpoint(response http.ResponseWriter, request *http.Request) {
+	username, _, _ := request.BasicAuth()
+	user, ok := dao.GetStorage().GetUser(username)
+	if !ok {
+		render.Render(response, request, ErrInternalServer(errors.New("")))
+		return
+	}
+	user.Password = ""
+	user.NewPassword = ""
+	user.Salt = []byte{}
+	render.JSON(response, request, user)
+}
+
+//GetUsersEndpoint getting all user infos
+func GetUsersEndpoint(response http.ResponseWriter, request *http.Request) {
+	users, err := dao.GetStorage().GetUsers()
+	if err != nil {
+		render.Render(response, request, ErrInternalServer(err))
+		return
+	}
+	myUsers := make([]model.User, 0)
+	for _, user := range users {
+		user.Password = ""
+		user.NewPassword = ""
+		user.Salt = []byte{}
+		myUsers = append(myUsers, user)
+	}
+	render.JSON(response, request, myUsers)
+}
+
+//GetUserEndpoint getting a user info
+func GetUserEndpoint(response http.ResponseWriter, request *http.Request) {
+	username := chi.URLParam(request, "username")
+	user, ok := dao.GetStorage().GetUser(username)
+	user.Password = ""
+	user.NewPassword = ""
+	user.Salt = []byte{}
+	if !ok {
+		render.Render(response, request, ErrInternalServer(errors.New("")))
+		return
+	}
+	render.JSON(response, request, user)
+}
+
+//PostUserEndpoint adding a new user
+func PostUserEndpoint(response http.ResponseWriter, request *http.Request) {
+	var user model.User
+	err := render.DefaultDecoder(request, &user)
+	if err != nil {
+		render.Render(response, request, ErrInvalidRequest(err))
+		return
+	}
+
+	adminusername, _, _ := request.BasicAuth()
+	admin, ok := dao.GetStorage().GetUser(adminusername)
+	if !ok {
+		render.Render(response, request, ErrInternalServer(errors.New("")))
+		return
+	}
+	if !admin.Admin {
+		render.Render(response, request, ErrForbidden)
+		return
+	}
+	idm := dao.GetIDM()
+
+	user, err = idm.AddUser(user)
+	if err != nil {
+		render.Render(response, request, ErrInvalidRequest(err))
+		return
+	}
+	user.Salt = []byte{}
+	user.Password = "#####"
+	user.NewPassword = ""
+	render.Status(request, http.StatusCreated)
+	render.JSON(response, request, user)
+}
+
+//PutUserEndpoint change password of user
+func PutUserEndpoint(response http.ResponseWriter, request *http.Request) {
+	username := chi.URLParam(request, "username")
+	var user model.User
+	err := render.DefaultDecoder(request, &user)
+	if err != nil {
+		render.Render(response, request, ErrInvalidRequest(err))
+		return
+	}
+	if username != user.Name {
+		render.Render(response, request, ErrInvalidRequest(errors.New("username should be identically")))
+		return
+	}
+	adminusername, _, _ := request.BasicAuth()
+	admin, ok := dao.GetStorage().GetUser(adminusername)
+	if !ok {
+		render.Render(response, request, ErrInternalServer(errors.New("")))
+		return
+	}
+	if (adminusername != username) && !admin.Admin {
+		render.Render(response, request, ErrForbidden)
+		return
+	}
+	idm := dao.GetIDM()
+
+	err = idm.ChangePWD(username, user.NewPassword, user.Password)
+	if err != nil {
+		render.Render(response, request, ErrInvalidRequest(err))
+		return
+	}
+	user.Salt = []byte{}
+	user.Password = "#####"
+	user.NewPassword = ""
+	render.JSON(response, request, user)
+}
+
+//DeleteUserEndpoint deleting a user
+func DeleteUserEndpoint(response http.ResponseWriter, request *http.Request) {
+	username := chi.URLParam(request, "username")
+	adminusername, _, _ := request.BasicAuth()
+	admin, ok := dao.GetStorage().GetUser(adminusername)
+	if !ok {
+		render.Render(response, request, ErrInternalServer(errors.New("")))
+		return
+	}
+	if !admin.Admin {
+		render.Render(response, request, ErrForbidden)
+		return
+	}
+	idm := dao.GetIDM()
+
+	err := idm.DeleteUser(username)
+	if err != nil {
+		render.Render(response, request, ErrInvalidRequest(err))
+		return
+	}
+	return
+}

+ 70 - 0
MessageService/cmd/health_test.go

@@ -0,0 +1,70 @@
+package main
+
+import (
+	"net/http"
+	"testing"
+)
+
+func TestReadinessCheck(t *testing.T) {
+	client := getClient()
+	resp, err := client.Get(baseURL + "/health/readiness")
+	if err != nil {
+		t.Errorf("Error getting response. %v", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		t.Errorf("Wrong statuscode: %d", resp.StatusCode)
+		return
+	}
+	t.Log("Readinesscheck OK.")
+}
+
+func TestHealthCheck(t *testing.T) {
+	client := getClient()
+	resp, err := client.Get(baseURL + "/health/health")
+	if err != nil {
+		t.Errorf("Error getting response. %v", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		t.Errorf("Wrong statuscode: %d", resp.StatusCode)
+		return
+	}
+	t.Log("Healthcheck OK.")
+}
+
+func TestSSLReadinessCheck(t *testing.T) {
+	client := getClient()
+	resp, err := client.Get(baseSslURL + "/health/readiness")
+	if err != nil {
+		t.Errorf("Error getting response. %v", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		t.Errorf("Wrong statuscode: %d", resp.StatusCode)
+		return
+	}
+	t.Log("SSL Readinesscheck OK.")
+}
+
+func TestSSLHealthCheck(t *testing.T) {
+	client := getClient()
+	resp, err := client.Get(baseSslURL + "/health/health")
+	if err != nil {
+		t.Errorf("Error getting response. %v", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		t.Errorf("Wrong statuscode: %d", resp.StatusCode)
+		return
+	}
+	t.Log("SSL Healthcheck OK.")
+}

+ 498 - 0
MessageService/cmd/service.go

@@ -0,0 +1,498 @@
+package main
+
+import (
+	"context"
+	"crypto/md5"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"os"
+	"os/signal"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+
+	api "github.com/willie68/AutoRestIoT/api"
+	"github.com/willie68/AutoRestIoT/dao"
+	"github.com/willie68/AutoRestIoT/health"
+	"github.com/willie68/AutoRestIoT/model"
+	"github.com/willie68/AutoRestIoT/worker"
+	"gopkg.in/yaml.v3"
+
+	"github.com/willie68/AutoRestIoT/internal/crypt"
+
+	consulApi "github.com/hashicorp/consul/api"
+	config "github.com/willie68/AutoRestIoT/config"
+	"github.com/willie68/AutoRestIoT/logging"
+
+	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/middleware"
+	"github.com/go-chi/render"
+
+	flag "github.com/spf13/pflag"
+)
+
+//apiVersion implementing api version for this service
+const apiVersion = "1"
+const servicename = "autorest-srv"
+
+var port int
+var sslport int
+var system string
+var serviceURL string
+var registryURL string
+var apikey string
+var ssl bool
+var configFile string
+var serviceConfig config.Config
+var consulAgent *consulApi.Agent
+var log logging.ServiceLogger
+
+func init() {
+	// variables for parameter override
+	ssl = false
+	log.Info("init service")
+	flag.IntVarP(&port, "port", "p", 0, "port of the http server.")
+	flag.IntVarP(&sslport, "sslport", "t", 0, "port of the https server.")
+	flag.StringVarP(&system, "systemid", "s", "", "this is the systemid of this service. Used for the apikey generation")
+	flag.StringVarP(&configFile, "config", "c", config.File, "this is the path and filename to the config file")
+	flag.StringVarP(&serviceURL, "serviceURL", "u", "", "service url from outside")
+	flag.StringVarP(&registryURL, "registryURL", "r", "", "registry url where to connect to consul")
+}
+
+func cors(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Access-Control-Allow-Origin", "*")
+		w.Header().Set("Access-Control-Allow-Methods", "*")
+		w.Header().Set("Access-Control-Allow-Headers", "*")
+		w.Header().Set("Access-Control-Allow-Credentials", "true")
+		log.Infof("Should set headers")
+
+		if r.Method == "OPTIONS" {
+			log.Infof("Should return for OPTIONS")
+			return
+		}
+		next.ServeHTTP(w, r)
+	})
+}
+
+func routes() *chi.Mux {
+	// sysApiHandler := api.NewSysAPIHandler(serviceConfig.SystemID, apikey)
+	baseURL := fmt.Sprintf("/api/v%s", apiVersion)
+	router := chi.NewRouter()
+	router.Use(
+		cors,
+		render.SetContentType(render.ContentTypeJSON),
+		middleware.Logger,
+		middleware.Compress(5),
+		middleware.Recoverer,
+		//sysApiHandler.Handler,
+	)
+
+	router.Route("/", func(r chi.Router) {
+
+		r.With(api.BasicAuth(servicename)).Mount(baseURL+"/models", api.ModelRoutes())
+		r.With(api.BasicAuth(servicename)).Mount(baseURL+"/files", api.FilesRoutes())
+		r.With(api.BasicAuth(servicename)).Mount(baseURL+"/users", api.UsersRoutes())
+		r.With(api.BasicAuth(servicename)).Mount(baseURL+"/"+api.AdminPrefix, api.AdminRoutes())
+
+		r.Mount("/health", health.Routes())
+	})
+
+	staticDir := serviceConfig.WebRoot
+	if staticDir != "" {
+		workDir, _ := os.Getwd()
+		staticFiles, err := filepath.Abs(filepath.Join(workDir, staticDir))
+		if err != nil {
+			log.Alertf("can't serve static files: %s", err.Error())
+			panic(1)
+		}
+		filesDir := http.Dir(staticFiles)
+		FileServer(router, "/files", filesDir)
+	}
+	return router
+}
+
+// GetPublicInfoHandler getting server info
+func GetPublicInfoHandler(response http.ResponseWriter, request *http.Request) {
+	log.Infof("GET: path: %s", request.URL.Path)
+
+	jsonObject := make([]string, 0)
+	jsonObject = append(jsonObject, "pub hund")
+	jsonObject = append(jsonObject, "pub katze")
+	jsonObject = append(jsonObject, "pub maus")
+
+	render.JSON(response, request, jsonObject)
+}
+
+// GetPrivateInfoHandler getting server info
+func GetPrivateInfoHandler(response http.ResponseWriter, request *http.Request) {
+	log.Infof("GET: path: %s", request.URL.Path)
+
+	jsonObject := make([]string, 0)
+	jsonObject = append(jsonObject, "prv hund")
+	jsonObject = append(jsonObject, "prv katze")
+	jsonObject = append(jsonObject, "prv maus")
+
+	render.JSON(response, request, jsonObject)
+}
+
+func healthRoutes() *chi.Mux {
+	router := chi.NewRouter()
+	router.Use(
+		cors,
+		render.SetContentType(render.ContentTypeJSON),
+		middleware.Logger,
+		middleware.Compress(5),
+		middleware.Recoverer,
+	)
+
+	router.Route("/", func(r chi.Router) {
+		r.Mount("/health", health.Routes())
+	})
+	return router
+}
+
+func main() {
+	log.Info("starting server")
+	_, err := crypt.GenerateRandomBytes(20)
+	if err != nil {
+		log.Alertf("can't generate secure salts: %s", err.Error())
+		panic(1)
+	}
+	flag.Parse()
+
+	config.File = configFile
+	if err := config.Load(); err != nil {
+		log.Alertf("can't load config file: %s", err.Error())
+	}
+	serviceConfig = config.Get()
+	initConfig()
+	initGraylog()
+
+	healthCheckConfig := health.CheckConfig(serviceConfig.HealthCheck)
+	backgroundTasksConfig := worker.BackgroundConfig(serviceConfig.BackgroundTasks)
+
+	defer log.Close()
+
+	if serviceConfig.SystemID == "" {
+		log.Fatal("system id not given, can't start! Please use config file or -s parameter")
+	}
+
+	gc := crypt.GenerateCertificate{
+		Organization: "MCS Media Computer Spftware",
+		Host:         "127.0.0.1",
+		ValidFor:     10 * 365 * 24 * time.Hour,
+		IsCA:         false,
+		EcdsaCurve:   "P256",
+		Ed25519Key:   true,
+	}
+
+	if serviceConfig.Sslport > 0 {
+		ssl = true
+		log.Info("ssl active")
+	}
+
+	storage := &dao.MongoDAO{}
+	storage.InitDAO(config.Get().MongoDB)
+	dao.SetStorage(storage)
+
+	// initialise the identity managment system
+	idm := dao.NewIDM()
+	idm.InitIDM()
+	dao.SetIDM(idm)
+
+	health.InitHealthSystem(healthCheckConfig)
+	worker.InitBackgroundTasks(backgroundTasksConfig)
+
+	apikey = getApikey()
+	log.Infof("systemid: %s", serviceConfig.SystemID)
+	log.Infof("apikey: %s", apikey)
+	log.Infof("ssl: %t", ssl)
+	log.Infof("serviceURL: %s", serviceConfig.ServiceURL)
+	if serviceConfig.RegistryURL != "" {
+		log.Infof("registryURL: %s", serviceConfig.RegistryURL)
+	}
+	router := routes()
+	walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
+		log.Infof("%s %s", method, route)
+		return nil
+	}
+
+	if err := chi.Walk(router, walkFunc); err != nil {
+		log.Alertf("Logging err: %s", err.Error())
+	}
+	log.Info("Health routes")
+	healthRouter := healthRoutes()
+	if err := chi.Walk(healthRouter, walkFunc); err != nil {
+		log.Alertf("Logging err: %s", err.Error())
+	}
+
+	initAutoRest()
+
+	var sslsrv *http.Server
+	var srv *http.Server
+	if ssl {
+		tlsConfig, err := gc.GenerateTLSConfig()
+		if err != nil {
+			log.Alertf("logging err: %s", err.Error())
+		}
+		sslsrv = &http.Server{
+			Addr:         "0.0.0.0:" + strconv.Itoa(serviceConfig.Sslport),
+			WriteTimeout: time.Second * 15,
+			ReadTimeout:  time.Second * 15,
+			IdleTimeout:  time.Second * 60,
+			Handler:      router,
+			TLSConfig:    tlsConfig,
+		}
+		go func() {
+			log.Infof("starting https server on address: %s", sslsrv.Addr)
+			if err := sslsrv.ListenAndServeTLS("", ""); err != nil {
+				log.Alertf("error starting server: %s", err.Error())
+			}
+		}()
+		srv = &http.Server{
+			Addr:         "0.0.0.0:" + strconv.Itoa(serviceConfig.Port),
+			WriteTimeout: time.Second * 15,
+			ReadTimeout:  time.Second * 15,
+			IdleTimeout:  time.Second * 60,
+			Handler:      healthRouter,
+		}
+		go func() {
+			log.Infof("starting http server on address: %s", srv.Addr)
+			if err := srv.ListenAndServe(); err != nil {
+				log.Alertf("error starting server: %s", err.Error())
+			}
+		}()
+	} else {
+		// own http server for the healthchecks
+		srv = &http.Server{
+			Addr:         "0.0.0.0:" + strconv.Itoa(serviceConfig.Port),
+			WriteTimeout: time.Second * 15,
+			ReadTimeout:  time.Second * 15,
+			IdleTimeout:  time.Second * 60,
+			Handler:      router,
+		}
+		go func() {
+			log.Infof("starting http server on address: %s", srv.Addr)
+			if err := srv.ListenAndServe(); err != nil {
+				log.Alertf("error starting server: %s", err.Error())
+			}
+		}()
+	}
+
+	if serviceConfig.RegistryURL != "" {
+		initRegistry()
+	}
+
+	//go importData("E:/temp/backup/schematic/dev")
+	//go generateTempData()
+
+	osc := make(chan os.Signal, 1)
+	signal.Notify(osc, os.Interrupt)
+	<-osc
+
+	log.Info("waiting for clients")
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
+	defer cancel()
+
+	srv.Shutdown(ctx)
+	if ssl {
+		sslsrv.Shutdown(ctx)
+	}
+
+	log.Info("finished")
+
+	os.Exit(0)
+}
+
+func initGraylog() {
+	log.GelfURL = serviceConfig.Logging.Gelfurl
+	log.GelfPort = serviceConfig.Logging.Gelfport
+	log.SystemID = serviceConfig.SystemID
+
+	log.InitGelf()
+}
+
+func initRegistry() {
+	//register to consul, if configured
+	consulConfig := consulApi.DefaultConfig()
+	consulURL, err := url.Parse(serviceConfig.RegistryURL)
+	consulConfig.Scheme = consulURL.Scheme
+	consulConfig.Address = fmt.Sprintf("%s:%s", consulURL.Hostname(), consulURL.Port())
+	consulClient, err := consulApi.NewClient(consulConfig)
+	if err != nil {
+		log.Alertf("can't connect to consul. %v", err)
+	}
+	consulAgent = consulClient.Agent()
+
+	check := new(consulApi.AgentServiceCheck)
+	check.HTTP = fmt.Sprintf("%s/health/health", serviceConfig.ServiceURL)
+	check.Timeout = (time.Minute * 1).String()
+	check.Interval = (time.Second * 30).String()
+	check.TLSSkipVerify = true
+	serviceDef := &consulApi.AgentServiceRegistration{
+		Name:  servicename,
+		Check: check,
+	}
+
+	err = consulAgent.ServiceRegister(serviceDef)
+
+	if err != nil {
+		log.Alertf("can't register to consul. %s", err)
+		time.Sleep(time.Second * 60)
+	}
+
+}
+
+func initConfig() {
+	if port > 0 {
+		serviceConfig.Port = port
+	}
+	if sslport > 0 {
+		serviceConfig.Sslport = sslport
+	}
+	if system != "" {
+		serviceConfig.SystemID = system
+	}
+	if serviceURL != "" {
+		serviceConfig.ServiceURL = serviceURL
+	}
+}
+
+func getApikey() string {
+	value := fmt.Sprintf("%s_%s", servicename, serviceConfig.SystemID)
+	apikey := fmt.Sprintf("%x", md5.Sum([]byte(value)))
+	return strings.ToLower(apikey)
+}
+
+func initAutoRest() {
+	backendPath := serviceConfig.BackendPath
+
+	var files []string
+	err := filepath.Walk(backendPath, func(path string, info os.FileInfo, err error) error {
+		if !info.IsDir() {
+			if strings.HasSuffix(info.Name(), ".yaml") {
+				files = append(files, path)
+			}
+		}
+		return nil
+	})
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	storage := dao.GetStorage()
+
+	route := model.Route{
+		Backend:  "_system",
+		Model:    "backends",
+		Apikey:   getApikey(),
+		SystemID: serviceConfig.SystemID,
+	}
+
+	worker.BackendStorageRoute = route
+
+	// importing the files, if needed, to the database
+	for _, value := range files {
+		data, err := ioutil.ReadFile(value)
+		bemodel := model.Backend{}
+		bemodel.DataSources = make([]model.DataSource, 0)
+		bemodel.Rules = make([]model.Rule, 0)
+		err = yaml.Unmarshal(data, &bemodel)
+		if err != nil {
+			log.Alertf("%v", err)
+			break
+		}
+
+		query := fmt.Sprintf("{\"backendname\": \"%s\"}", bemodel.Backendname)
+		count, _, err := storage.QueryModel(route, query, 0, 10)
+		if err != nil {
+			log.Alertf("%v", err)
+			break
+		}
+		if count == 0 {
+			id, err := worker.StoreBackend(bemodel)
+			if err != nil {
+				log.Alertf("%v", err)
+				break
+			}
+			log.Infof("model created : %s", id)
+		}
+
+	}
+
+	//now, getting all backends from the database and register them
+	query := ""
+	count, backends, err := storage.QueryModel(route, query, 0, 10)
+	if err != nil {
+		log.Alertf("%v", err)
+		return
+	}
+	if count > 0 {
+		for _, dbmodel := range backends {
+			jsonString, err := json.Marshal(dbmodel)
+			if err != nil {
+				log.Alertf("%v", err)
+				break
+			}
+			bemodel := model.Backend{}
+			bemodel.DataSources = make([]model.DataSource, 0)
+			bemodel.Rules = make([]model.Rule, 0)
+
+			err = json.Unmarshal(jsonString, &bemodel)
+			if err != nil {
+				log.Alertf("%v", err)
+				break
+			}
+
+			err = registerBackend(bemodel)
+			if err != nil {
+				log.Alertf("%v", err)
+				break
+			}
+		}
+	}
+}
+
+func registerBackend(bemodel model.Backend) error {
+	bemodel, err := worker.PrepareBackend(bemodel)
+	if err != nil {
+		return fmt.Errorf("validating backend %s, getting error: %v", bemodel.Backendname, err)
+	}
+	err = worker.ValidateBackend(bemodel)
+	if err != nil {
+		return fmt.Errorf("validating backend %s, getting error: %v", bemodel.Backendname, err)
+	}
+	err = worker.RegisterBackend(bemodel)
+	if err != nil {
+		return fmt.Errorf("error registering backend %s. %v", bemodel.Backendname, err)
+	}
+	backendName := model.BackendList.Add(bemodel)
+	log.Infof("registering backend %s successfully.", backendName)
+	return nil
+}
+
+// FileServer conveniently sets up a http.FileServer handler to serve
+// static files from a http.FileSystem.
+func FileServer(r chi.Router, path string, root http.FileSystem) {
+	if strings.ContainsAny(path, "{}*") {
+		panic("FileServer does not permit any URL parameters.")
+	}
+
+	if path != "/" && path[len(path)-1] != '/' {
+		r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
+		path += "/"
+	}
+	path += "*"
+
+	r.Get(path, func(w http.ResponseWriter, r *http.Request) {
+		rctx := chi.RouteContext(r.Context())
+		pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
+		fs := http.StripPrefix(pathPrefix, http.FileServer(root))
+		fs.ServeHTTP(w, r)
+	})
+}

+ 78 - 0
MessageService/cmd/service_test.go

@@ -0,0 +1,78 @@
+package main
+
+import (
+	"bytes"
+	"crypto/md5"
+	"crypto/tls"
+	"fmt"
+	"net/http"
+	"strings"
+
+	api "github.com/willie68/AutoRestIoT/api"
+)
+
+const baseURL = "http://127.0.0.1:9080"
+const baseSslURL = "https://127.0.0.1:9443"
+const restEndpoints = "/api/v1/"
+const username = "admin"
+const passwd = "admin"
+const systemID = "autorest-srv"
+
+var AdminUser = BasicUser{name: "admin", pwd: "admin"}
+var EditorUser = BasicUser{name: "editor", pwd: "editor"}
+var GuestUser = BasicUser{name: "guest", pwd: "guest"}
+
+type BasicUser struct {
+	name string
+	pwd  string
+}
+
+func getClient() http.Client {
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+	}
+	return http.Client{Transport: tr}
+}
+
+func getTestApikey() string {
+	value := fmt.Sprintf("%s_%s", servicename, systemID)
+	apikey := fmt.Sprintf("%x", md5.Sum([]byte(value)))
+	return strings.ToLower(apikey)
+}
+
+func getGetRequest(url string, user BasicUser) (*http.Response, error) {
+	client := getClient()
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.SetBasicAuth(user.name, user.pwd)
+	req.Header.Add(api.APIKeyHeader, getTestApikey())
+	req.Header.Add(api.SystemHeader, systemID)
+	return client.Do(req)
+}
+
+func getPostRequest(url string, user BasicUser, payload []byte) (*http.Response, error) {
+	client := getClient()
+	req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
+	if err != nil {
+		return nil, err
+	}
+	req.SetBasicAuth(user.name, user.pwd)
+	req.Header.Add(api.APIKeyHeader, getTestApikey())
+	req.Header.Add(api.SystemHeader, systemID)
+	req.Header.Set("Content-Type", "application/json")
+	return client.Do(req)
+}
+
+func getDeleteRequest(url string, user BasicUser) (*http.Response, error) {
+	client := getClient()
+	req, err := http.NewRequest("DELETE", url, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.SetBasicAuth(user.name, user.pwd)
+	req.Header.Add(api.APIKeyHeader, getTestApikey())
+	req.Header.Add(api.SystemHeader, systemID)
+	return client.Do(req)
+}

+ 114 - 0
MessageService/cmd/userapi_test.go

@@ -0,0 +1,114 @@
+package main
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/willie68/AutoRestIoT/model"
+)
+
+func TestGetUsers(t *testing.T) {
+	url := baseSslURL + restEndpoints + "users"
+	resp, err := getGetRequest(url, AdminUser)
+	if err != nil {
+		t.Errorf("Error getting response. %v", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		t.Errorf("Wrong statuscode: %d", resp.StatusCode)
+		return
+	}
+
+	bodyText, err := ioutil.ReadAll(resp.Body)
+	var users []model.User
+	err = json.Unmarshal(bodyText, &users)
+	if err != nil {
+		t.Errorf("Error ummarshalling response. %v", err)
+		return
+	}
+
+	t.Logf("response: url: %s, response: %v", url, users)
+	if len(users) != 3 {
+		t.Errorf("predefined user count not correct: %d", len(users))
+		return
+	}
+
+	t.Log("Get users OK.")
+}
+
+func TestPostUser(t *testing.T) {
+	url := baseSslURL + restEndpoints + "users"
+
+	newUser := model.User{
+		Name:     "newuser",
+		Password: "newuser",
+		Admin:    false,
+		Guest:    false,
+		Roles:    []string{"admin", "edit", "read"},
+	}
+	basicUser := BasicUser{name: newUser.Name, pwd: newUser.Password}
+
+	payload, err := json.Marshal(newUser)
+	if err != nil {
+		t.Errorf("Error marshall new user. %v", err)
+		return
+	}
+
+	resp, err := getPostRequest(url, AdminUser, payload)
+	if err != nil {
+		t.Errorf("Error getting response. %v", err)
+		return
+	}
+	defer resp.Body.Close()
+	bodyText, err := ioutil.ReadAll(resp.Body)
+	s := string(bodyText)
+	assert.Equal(t, http.StatusCreated, resp.StatusCode, "Wrong statuscode: %d, %s", resp.StatusCode, s)
+
+	var user model.User
+	err = json.Unmarshal(bodyText, &user)
+	if err != nil {
+		t.Errorf("Error ummarshalling response. %v", err)
+		return
+	}
+
+	t.Logf("response: url: %s, response: %v", url, user)
+	assert.Equal(t, newUser.Name, user.Name, "user name not identically")
+	assert.Equal(t, "#####", user.Password, "password not identically")
+	assert.Equal(t, newUser.Admin, user.Admin, "admin not identically")
+	assert.Equal(t, newUser.Guest, user.Guest, "guest not identically")
+	assert.Contains(t, user.Roles, "admin", "roles not inserted")
+	assert.Contains(t, user.Roles, "edit", "roles not inserted")
+	assert.Contains(t, user.Roles, "read", "roles not inserted")
+
+	url = baseSslURL + restEndpoints + "users/" + basicUser.name
+	resp, err = getGetRequest(url, basicUser)
+	if err != nil {
+		t.Errorf("Error getting response. %v", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		t.Errorf("Wrong statuscode: %d", resp.StatusCode)
+		return
+	}
+
+	resp, err = getDeleteRequest(url, AdminUser)
+	if err != nil {
+		t.Errorf("Error getting response. %v", err)
+		return
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		t.Errorf("Wrong statuscode: %d", resp.StatusCode)
+		return
+	}
+
+	t.Log("Get users OK.")
+}

+ 73 - 0
MessageService/config/loader.go

@@ -0,0 +1,73 @@
+package config
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	"github.com/willie68/AutoRestIoT/logging"
+	"gopkg.in/yaml.v3"
+)
+
+var config = Config{
+	Port:                  9080,
+	Sslport:               9443,
+	ServiceURL:            "http://127.0.0.1",
+	SystemID:              "autorest-srv",
+	AllowAnonymousBackend: false,
+
+	HealthCheck: HealthCheck{
+		Period: 30,
+	},
+}
+
+// File the config file
+var File = "config/service.yaml"
+var log logging.ServiceLogger
+
+// Get returns loaded config
+func Get() Config {
+	return config
+}
+
+// Load loads the config
+func Load() error {
+	_, err := os.Stat(File)
+	if err != nil {
+		return err
+	}
+	data, err := ioutil.ReadFile(File)
+	if err != nil {
+		return fmt.Errorf("can't load config file: %s", err.Error())
+	}
+	err = yaml.Unmarshal(data, &config)
+	if err != nil {
+		return fmt.Errorf("can't unmarshal config file: %s", err.Error())
+	}
+	readSecret()
+	value, _ := yaml.Marshal(config)
+	log.Infof("using configfile %s. \nconfiguration:\n%s\n", File, string(value))
+	return nil
+}
+
+func readSecret() error {
+	secretFile := config.SecretFile
+	if secretFile != "" {
+		data, err := ioutil.ReadFile(secretFile)
+		if err != nil {
+			return fmt.Errorf("can't load secret file: %s", err.Error())
+		}
+		var secretConfig Secret = Secret{}
+		err = yaml.Unmarshal(data, &secretConfig)
+		if err != nil {
+			return fmt.Errorf("can't unmarshal secret file: %s", err.Error())
+		}
+		mergeSecret(secretConfig)
+	}
+	return nil
+}
+
+func mergeSecret(secret Secret) {
+	config.MongoDB.Username = secret.MongoDB.Username
+	config.MongoDB.Password = secret.MongoDB.Password
+}

+ 11 - 0
MessageService/config/secret.go

@@ -0,0 +1,11 @@
+package config
+
+/*
+Secret our service configuration
+*/
+type Secret struct {
+	MongoDB struct {
+		Username string `yaml:"username"`
+		Password string `yaml:"password"`
+	} `yaml:"mongodb"`
+}

+ 53 - 0
MessageService/configs/backends/schematic.yaml

@@ -0,0 +1,53 @@
+backendname: schematics
+description: Willies World Schematics Database
+models:
+    - name: schematic
+      fields: 
+        - name: Manufacturer
+          type: string
+          mandatory: true
+          collection: false
+        - name: Model
+          type: string
+          mandatory: true
+          collection: false
+        - name: Tags
+          type: string
+          mandatory: false
+          collection: true
+        - name: Files
+          type: file
+          mandatory: false
+          collection: true
+      indexes:
+        - name: $fulltext
+          fields:
+            - Manufacturer
+            - Model
+            - Tags
+        - name: Foreignid
+          unique: true
+          fields:
+            - Foreignid
+    - name: manufacturers
+      fields: 
+        - name: name
+          type: string
+          mandatory: true
+          collection: false
+      indexes:
+        - name: name
+          unique: true
+          fields:
+            - name
+    - name: tags
+      fields: 
+        - name: name
+          type: string
+          mandatory: true
+          collection: false
+      indexes:
+        - name: name
+          unique: true
+          fields:
+            - name

+ 192 - 0
MessageService/configs/backends/sensor.yaml

@@ -0,0 +1,192 @@
+backendname: sensors
+description: sensor model für storing and retrieving sensor data
+models:
+  - name: temperatur
+    fields:
+      - name: temperatur
+        type: float
+        mandatory: false
+        collection: false
+      - name: vstring
+        type: string
+        mandatory: false
+        collection: false
+      - name: vint
+        type: int
+        mandatory: false
+        collection: false
+      - name: vfloat
+        type: float
+        mandatory: false
+        collection: false
+      - name: vtime
+        type: time
+        mandatory: false
+        collection: false
+      - name: vbool
+        type: bool
+        mandatory: false
+        collection: false
+      - name: source
+        type: string
+        mandatory: false
+        collection: false
+datasources:
+  - name: temp_kueche
+    type: mqtt
+    destinations: 
+      - $model.temperatur 
+      - mqtt_sensors_temperatur
+    rule: tasmota_ds18b20
+    config: 
+      broker: 127.0.0.1:1883
+      topic: tele/tasmota_63E6F8/SENSOR
+      qos: 0
+      payload: application/json
+      username: temp
+      password: temp
+      addTopicAsAttribute: topic
+  - name: temp_arbeit
+    type: mqtt
+    destinations: 
+      - mqtt_hm_output
+    rule: tasmota_ds18b20_hm
+    config: 
+      broker: 192.168.178.12:1883
+      topic: tele/tasmota_63E6F8/SENSOR
+      qos: 0
+      payload: application/json
+      username: temp
+      password: temp
+      addTopicAsAttribute: topic
+  - name: temp_simple_text
+    type: mqtt
+    destinations: 
+      - $model.temperatur 
+    config: 
+      broker: 127.0.0.1:1883
+      topic: stat/temperatur/simple/string
+      payload: application/x.simple
+      username: temp
+      password: temp
+      addTopicAsAttribute: topic
+      simpleValueAttribute: vstring
+      simpleValueAttributeType: string
+  - name: temp_simple_int
+    type: mqtt
+    destinations: 
+      - $model.temperatur 
+    config: 
+      broker: 127.0.0.1:1883
+      topic: stat/temperatur/simple/int
+      payload: application/x.simple
+      username: temp
+      password: temp
+      addTopicAsAttribute: topic
+      simpleValueAttribute: vint
+      simpleValueAttributeType: int
+  - name: temp_simple_float
+    type: mqtt
+    destinations: 
+      - $model.temperatur 
+    config: 
+      broker: 127.0.0.1:1883
+      topic: stat/temperatur/simple/float
+      payload: application/x.simple
+      username: temp
+      password: temp
+      addTopicAsAttribute: topic
+      simpleValueAttribute: vfloat
+      simpleValueAttributeType: float
+  - name: temp_simple_time
+    type: mqtt
+    destinations: 
+      - $model.temperatur 
+    config: 
+      broker: 127.0.0.1:1883
+      topic: stat/temperatur/simple/time
+      payload: application/x.simple
+      username: temp
+      password: temp
+      addTopicAsAttribute: topic
+      simpleValueAttribute: vtime
+      simpleValueAttributeType: time
+  - name: temp_simple_bool
+    type: mqtt
+    destinations: 
+      - $model.temperatur 
+    config: 
+      broker: 127.0.0.1:1883
+      topic: stat/temperatur/simple/bool
+      payload: application/x.simple
+      username: temp
+      password: temp
+      addTopicAsAttribute: topic
+      simpleValueAttribute: vbool
+      simpleValueAttributeType: bool
+rules:
+  - name: tasmota_ds18b20
+    description: transforming the tasmota json structure of the DS18B20 into my simple structure
+    transform: 
+      - operation: shift
+        spec: 
+          Temperature: DS18B20.Temperature
+          TempUnit: TempUnit
+          Datetime: Time
+          Timestamp: Time
+          Msg: $
+      - operation: timestamp
+        spec:
+          Timestamp: {
+            inputFormat: 2006-01-02T15:04:05,
+            outputFormat: $unixext
+          }
+  - name: hm_temp_simple
+    description: handle homematic temperatur rightly
+    transform:
+      - operation: shift
+        spec: 
+          Temperature: val
+          Timestamp: ts
+          Datetime: ts
+      - operation: default
+        spec: 
+          TempUnit: C
+      - operation: timestamp
+        spec:
+          Datetime: {
+            inputFormat: $unixext,
+            outputFormat: 2006-01-02T15:04:05-0700
+          }
+  - name: tasmota_ds18b20_hm
+    description: transforming the tasmota json structure of the DS18B20 into hm basic structure
+    transform: 
+      - operation: shift
+        spec: 
+          val: DS18B20.Temperature
+          ts: Time
+      - operation: timestamp
+        spec:
+          Timestamp: {
+            inputFormat: 2006-01-02T15:04:05,
+            outputFormat: $unixext
+          }
+destinations:
+  - name: mqtt_sensors_temperatur
+    type: mqtt
+    config: 
+      broker: 127.0.0.1:1883
+      topic: stat/temperatur
+      qos: 1
+      payload: application/json
+      username: temp
+      password: temp
+  - name: mqtt_hm_output
+    type: mqtt
+    config: 
+      broker: 192.168.178.12:1883
+      topic: hm/set/TempArbeit
+      qos: 0
+      payload: application/json
+      username: temp
+      password: temp

+ 39 - 0
MessageService/configs/backends/simple.yaml

@@ -0,0 +1,39 @@
+backendname: mybe
+models:
+  - name: mymodel
+    fields:
+      - name: title
+        type: string
+        mandatory: true
+        collection: false
+      - name: array
+        type: string
+        mandatory: false
+        collection: true
+      - name: file
+        type: file
+        mandatory: false
+        collection: false
+      - name: date
+        type: time
+        mandatory: false
+        collection: false
+      - name: bool_1
+        type: bool
+        mandatory: false
+        collection: false
+      - name: bool_2
+        type: bool
+        mandatory: false
+        collection: false
+    indexes:
+      - name: $fulltext
+        fields:
+          - title
+          - tags
+      - name: title
+        fields:
+          - title
+      - name: array
+        fields:
+          - array

+ 3 - 0
MessageService/configs/secret.yaml

@@ -0,0 +1,3 @@
+mongodb:
+    username: backend1
+    password: backend1

+ 34 - 0
MessageService/configs/service.yaml

@@ -0,0 +1,34 @@
+# port of the http server
+port: 8080 
+# port of the https server
+sslport: 8443
+# this is the servicURL from outside
+serviceURL: http://127.0.0.1:8080
+# this is the registry URL from inside this service for consul as service registry
+registryURL: 
+# this is the system id of this service. services in a cluster mode should have the same system id.
+systemID: autorest-srv
+#sercret file for storing usernames and passwords
+secretfile: /tmp/storage/config/secret.yaml
+#where the configuration files of the backends are
+backendpath: configs/backends
+#allow data saving without a registered backend
+allowAnonymousBackend: true
+
+# configuration of the gelf logging server
+logging:
+    gelf-url: 
+    gelf-port: 
+
+# healthcheck configuration
+healthcheck:
+    # automatically check the health of this service every ## seconds
+    period: 30
+# configuration of the mongo storage
+mongodb:
+    host: 127.0.0.1 #mongo host ip (comma seperated ip's with a cluster installation )
+    port: 27017 # mongo host port
+    username: #username for the database (should be at last dbadmin, and read/write access)
+    password: #password for the user
+    authdb: backend1 # database to authenticate against
+    database: backend1 # database for the data

+ 30 - 0
MessageService/configs/serviceLocal.yaml

@@ -0,0 +1,30 @@
+# port of the http server
+port: 9080 
+# port of the https server
+sslport: 0
+# this is the servicURL from outside
+serviceURL: http://127.0.0.1:9080
+# this is the registry URL from inside
+registryURL: 
+# this is the system id of this service. services in a cluster mode should have the same system id.
+systemID: autorest-srv
+#sercret file for storing usernames and passwords
+secretfile: configs/secret.yaml
+#where the configuration of the backends are
+backendpath: configs/backends
+#allow data saving without a registered backend
+allowAnonymousBackend: true
+
+logging:
+    gelf-url: 
+    gelf-port: 
+
+healthcheck:
+    period: 30
+mongodb:
+    host: 127.0.0.1
+    port: 27017
+    username:
+    password:
+    authdb: backend1
+    database: backend1

+ 36 - 0
MessageService/configs/service_home.yaml

@@ -0,0 +1,36 @@
+# port of the http server
+port: 9080 
+# port of the https server
+sslport: 9443
+# this is the servicURL from outside
+serviceURL: https://127.0.0.1:9443
+# this is the registry URL from inside
+registryURL: 
+# this is the system id of this service. services in a cluster mode should have the same system id.
+systemID: autorest-srv
+#sercret file for storing usernames and passwords
+secretfile: configs/secret.yaml
+#where the configuration of the backends are
+backendpath: configs/backends
+#allow data saving without a registered backend
+allowAnonymousBackend: true
+webRoot: ../autorestui/dist
+
+logging:
+    gelf-url: 
+    gelf-port: 
+
+healthcheck:
+    period: 30
+
+backgroundtasks:
+    period: 86400
+    deleteOrphanedFiles: true
+
+mongodb:
+    host: 192.168.178.14
+    port: 27017
+    username:
+    password:
+    authdb: backend1
+    database: backend1

+ 15 - 0
MessageService/dao/errors.go

@@ -0,0 +1,15 @@
+package dao
+
+import "errors"
+
+//ErrNoDocument the requested document was not found in the backend
+var ErrNoDocument = errors.New("Document not found")
+
+//ErrUniqueIndexError there is a unique index violation
+var ErrUniqueIndexError = errors.New("Unique index error")
+
+//ErrNotImplemented the desired feature/function/method is not implemented
+var ErrNotImplemented = errors.New("Not implemented")
+
+//ErrUnknownError i don't know what this error is about
+var ErrUnknownError = errors.New("Unknown server error")

+ 263 - 0
MessageService/dao/idm.go

@@ -0,0 +1,263 @@
+package dao
+
+/*
+  This is the identity management system for the AutoRest service. Here you will find all methods regarding the identity of a user,
+  authentication and authorisation.
+*/
+
+import (
+	"crypto/sha1"
+	"errors"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/willie68/AutoRestIoT/internal/slicesutils"
+	"github.com/willie68/AutoRestIoT/model"
+	"golang.org/x/crypto/pbkdf2"
+)
+
+//IDM the idm cache struct with users and salts
+type IDM struct {
+	users map[string]string
+	salts map[string][]byte
+}
+
+const hashPrefix = "hash:"
+
+// time to reload all users
+const userReloadPeriod = 1 * time.Hour
+
+var idm IDM
+
+//NewIDM creating a new idm object
+func NewIDM() IDM {
+	return IDM{
+		users: make(map[string]string),
+		salts: make(map[string][]byte),
+	}
+}
+
+//GetIDM getting the actual idm object
+func GetIDM() IDM {
+	return idm
+}
+
+//SetIDM setting the actual idm object
+func SetIDM(newIdm IDM) {
+	idm = newIdm
+}
+
+//BuildPasswordHash building a hash value of the password
+func BuildPasswordHash(password string, salt []byte) string {
+	if !strings.HasPrefix(password, hashPrefix) {
+		hash := pbkdf2.Key([]byte(password), salt, 4096, 32, sha1.New)
+		// hash := md5.Sum([]byte(password))
+		password = fmt.Sprintf("%s:%x", hashPrefix, hash)
+	}
+	return password
+}
+
+//InitIDM initialise the local idm system
+func (i *IDM) InitIDM() {
+	i.initUsers()
+}
+
+func (i *IDM) initUsers() {
+	i.reloadUsers()
+
+	go func() {
+		background := time.NewTicker(userReloadPeriod)
+		for range background.C {
+			i.reloadUsers()
+		}
+	}()
+}
+
+//GetSalt getting the salt for a user.
+func (i *IDM) GetSalt(username string) ([]byte, bool) {
+	username = strings.ToLower(username)
+	salt, ok := i.salts[username]
+	if ok {
+		return salt, true
+	}
+
+	return []byte{}, false
+}
+
+func (i *IDM) reloadUsers() {
+	users, err := GetStorage().GetUsers()
+	if err != nil {
+		log.Alertf("error loading users: %v", err)
+		return
+	}
+	localUsers := make(map[string]string)
+	localSalts := make(map[string][]byte)
+	for _, user := range users {
+		username := user.Name
+		password := user.Password
+		salt := user.Salt
+		localSalts[username] = salt
+		localUsers[username] = BuildPasswordHash(password, salt)
+	}
+	i.users = localUsers
+	i.salts = localSalts
+	if len(i.users) == 0 {
+		admin := model.User{
+			Name:      "admin",
+			Firstname: "",
+			Lastname:  "Admin",
+			Password:  "admin",
+			Admin:     true,
+			Roles:     []string{"admin"},
+		}
+		user, err := GetStorage().AddUser(admin)
+		if err != nil {
+			log.Alertf("error adding user: %v", err)
+			return
+		}
+		i.addUserToMap(user)
+
+		editor := model.User{
+			Name:      "editor",
+			Firstname: "",
+			Lastname:  "Editor",
+			Password:  "editor",
+			Admin:     false,
+			Guest:     false,
+			Roles:     []string{"edit"},
+		}
+		user, err = GetStorage().AddUser(editor)
+		if err != nil {
+			log.Alertf("error adding user: %v", err)
+			return
+		}
+		i.addUserToMap(user)
+
+		guest := model.User{
+			Name:      "guest",
+			Firstname: "",
+			Lastname:  "Guest",
+			Password:  "guest",
+			Admin:     false,
+			Guest:     true,
+			Roles:     []string{"read"},
+		}
+		user, err = GetStorage().AddUser(guest)
+		if err != nil {
+			log.Alertf("error adding user: %v", err)
+			return
+		}
+		i.addUserToMap(user)
+	}
+}
+
+//CheckUser checking username and password... returns true if the user is active and the password for this user is correct
+func (i *IDM) CheckUser(username string, password string) bool {
+	username = strings.ToLower(username)
+	pwd, ok := i.users[username]
+	if ok {
+		if pwd == password {
+			return true
+		}
+		user, ok := GetStorage().GetUser(username)
+		if ok {
+			if user.Password == password {
+				//change password on another node
+				i.addUserToMap(user)
+				return true
+			}
+		}
+	}
+
+	if !ok {
+		user, ok := GetStorage().GetUser(username)
+		if ok {
+			if user.Password == password {
+				//change password on another node
+				i.addUserToMap(user)
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+//UserInRoles is a user in the given role
+func (i *IDM) UserInRoles(username string, roles []string) bool {
+	user, ok := GetStorage().GetUser(username)
+	if !ok {
+		return false
+	}
+
+	for _, role := range roles {
+		if slicesutils.Contains(user.Roles, role) {
+			return true
+		}
+	}
+	return false
+}
+
+// AddUser adding a new user to the system
+func (i *IDM) AddUser(user model.User) (model.User, error) {
+	if user.Name == "" {
+		return model.User{}, errors.New("username should not be empty")
+	}
+	user.Name = strings.ToLower(user.Name)
+	_, ok := GetStorage().GetUser(user.Name)
+	if ok {
+		return model.User{}, errors.New("username already exists")
+	}
+
+	user, err := GetStorage().AddUser(user)
+	if err != nil {
+		return model.User{}, err
+	}
+	return user, nil
+}
+
+//DeleteUser deletes a user from the idm
+func (i *IDM) DeleteUser(username string) error {
+	err := GetStorage().DeleteUser(username)
+	if err != nil {
+		return err
+	}
+	delete(i.users, username)
+	delete(i.salts, username)
+	return nil
+}
+
+// ChangePWD changes the apssword of a single user
+func (i *IDM) ChangePWD(username string, newpassword string, oldpassword string) error {
+	if username == "" {
+		return errors.New("username should not be empty")
+	}
+	username = strings.ToLower(username)
+	pwd, ok := i.users[username]
+	if !ok {
+		return errors.New("username not registered")
+	}
+	usermodel, ok := GetStorage().GetUser(username)
+	if !ok {
+		return errors.New("username not registered")
+	}
+
+	oldpassword = BuildPasswordHash(oldpassword, usermodel.Salt)
+	if pwd != oldpassword {
+		return errors.New("actual password incorrect")
+	}
+
+	user, err := GetStorage().ChangePWD(username, newpassword)
+	if err != nil {
+		return err
+	}
+
+	i.addUserToMap(user)
+	return nil
+}
+
+func (i *IDM) addUserToMap(user model.User) {
+	i.users[user.Name] = user.Password
+	i.salts[user.Name] = user.Salt
+}

+ 718 - 0
MessageService/dao/mongodao.go

@@ -0,0 +1,718 @@
+package dao
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/willie68/AutoRestIoT/config"
+	"github.com/willie68/AutoRestIoT/internal"
+	"github.com/willie68/AutoRestIoT/internal/crypt"
+	"github.com/willie68/AutoRestIoT/internal/slicesutils"
+	"github.com/willie68/AutoRestIoT/logging"
+	"github.com/willie68/AutoRestIoT/model"
+
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/bson/primitive"
+	"go.mongodb.org/mongo-driver/mongo"
+	"go.mongodb.org/mongo-driver/mongo/gridfs"
+	"go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const timeout = 1 * time.Minute
+const attachmentsCollectionName = "attachments"
+const usersCollectionName = "users"
+
+// MongoDAO a mongodb based dao
+type MongoDAO struct {
+	initialised bool
+	client      *mongo.Client
+	mongoConfig config.MongoDB
+	bucket      gridfs.Bucket
+	database    mongo.Database
+	ticker      time.Ticker
+	done        chan bool
+}
+
+var log logging.ServiceLogger
+
+//InitDAO initialise the mongodb connection, build up all collections and indexes
+func (m *MongoDAO) InitDAO(MongoConfig config.MongoDB) {
+	m.initialised = false
+	m.mongoConfig = MongoConfig
+	//	uri := fmt.Sprintf("mongodb://%s:%s@%s:%d", mongoConfig.Username, mongoConfig.Password, mongoConfig.Host, mongoConfig.Port)
+	uri := fmt.Sprintf("mongodb://%s:%d", m.mongoConfig.Host, m.mongoConfig.Port)
+	clientOptions := options.Client()
+	clientOptions.ApplyURI(uri)
+	clientOptions.Auth = &options.Credential{Username: m.mongoConfig.Username, Password: m.mongoConfig.Password, AuthSource: m.mongoConfig.AuthDB}
+	var err error
+	m.client, err = mongo.NewClient(clientOptions)
+	if err != nil {
+		fmt.Printf("error: %s\n", err.Error())
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	err = m.client.Connect(ctx)
+	if err != nil {
+		fmt.Printf("error: %s\n", err.Error())
+	}
+	m.database = *m.client.Database(m.mongoConfig.Database)
+
+	myBucket, err := gridfs.NewBucket(&m.database, options.GridFSBucket().SetName(attachmentsCollectionName))
+	if err != nil {
+		fmt.Printf("error: %s\n", err.Error())
+	}
+	m.bucket = *myBucket
+
+	m.initialised = true
+}
+
+//ProcessFiles adding a file to the storage, stream like
+func (m *MongoDAO) ProcessFiles(RemoveCallback func(fileInfo model.FileInfo) bool) error {
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	cursor, err := m.bucket.Find(bson.M{}, &options.GridFSFindOptions{})
+	if err != nil {
+		fmt.Printf("error: %s\n", err.Error())
+		return err
+	}
+	defer cursor.Close(ctx)
+	count := 0
+	for cursor.Next(ctx) {
+		var file bson.M
+		if err = cursor.Decode(&file); err != nil {
+			log.Alertf("%v", err)
+			continue
+		}
+		metadata := file["metadata"].(bson.M)
+		info := model.FileInfo{}
+		info.Filename = file["filename"].(string)
+		info.Backend = metadata["backend"].(string)
+		info.ID = file["_id"].(primitive.ObjectID).Hex()
+		info.UploadDate = file["uploadDate"].(primitive.DateTime).Time()
+		RemoveCallback(info)
+		count++
+	}
+	return nil
+}
+
+//AddFile adding a file to the storage, stream like
+func (m *MongoDAO) AddFile(backend string, filename string, reader io.Reader) (string, error) {
+	uploadOpts := options.GridFSUpload().SetMetadata(bson.D{{Key: "backend", Value: backend}})
+
+	fileID, err := m.bucket.UploadFromStream(filename, reader, uploadOpts)
+	if err != nil {
+		fmt.Printf("error: %s\n", err.Error())
+		return "", err
+	}
+	log.Infof("Write file to DB was successful. File id: %s \n", fileID)
+	id := fileID.Hex()
+	return id, nil
+}
+
+//GetFilename getting the filename of an attachment from the database with the id
+func (m *MongoDAO) GetFilename(backend string, fileid string) (string, error) {
+	objectID, err := primitive.ObjectIDFromHex(fileid)
+	if err != nil {
+		log.Alertf("%v", err)
+		return "", err
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	cursor, err := m.bucket.Find(bson.M{"_id": objectID, "metadata.backend": backend})
+	if err != nil {
+		log.Alertf("%v", err)
+		return "", err
+	}
+	defer cursor.Close(ctx)
+	cursor.Next(ctx)
+	var file bson.M
+	var filename string
+	if err = cursor.Decode(&file); err != nil {
+		log.Alertf("%v", err)
+		return "", err
+	}
+	filename = file["filename"].(string)
+	return filename, nil
+}
+
+//GetFile getting a single file from the database with the id
+func (m *MongoDAO) GetFile(backend string, fileid string, stream io.Writer) error {
+	_, err := m.GetFilename(backend, fileid)
+	if err != nil {
+		log.Alertf("%v", err)
+		return err
+	}
+
+	objectID, err := primitive.ObjectIDFromHex(fileid)
+	if err != nil {
+		log.Alertf("%v", err)
+		return err
+	}
+	_, err = m.bucket.DownloadToStream(objectID, stream)
+	if err != nil {
+		log.Alertf("%v", err)
+		return err
+	}
+	return nil
+}
+
+//DeleteFile getting a single from the database with the id
+func (m *MongoDAO) DeleteFile(backend string, fileid string) error {
+	_, err := m.GetFilename(backend, fileid)
+	if err != nil {
+		log.Alertf("%v", err)
+		return err
+	}
+
+	objectID, err := primitive.ObjectIDFromHex(fileid)
+	if err != nil {
+		log.Alertf("%v", err)
+		return err
+	}
+	err = m.bucket.Delete(objectID)
+	if err != nil {
+		log.Alertf("%v", err)
+		return err
+	}
+	return nil
+}
+
+//GetUsers getting a list of users
+func (m *MongoDAO) GetUsers() ([]model.User, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	usersCollection := m.database.Collection(usersCollectionName)
+	filter := bson.M{}
+	cursor, err := usersCollection.Find(ctx, filter)
+	if err != nil {
+		fmt.Printf("error: %s\n", err.Error())
+		return nil, err
+	}
+	defer cursor.Close(ctx)
+	users := make([]model.User, 0)
+	for cursor.Next(ctx) {
+		var user model.User
+		if err = cursor.Decode(&user); err != nil {
+			log.Alertf("%v", err)
+			return nil, err
+		}
+		users = append(users, user)
+	}
+
+	sort.Slice(users, func(i, j int) bool {
+		return users[i].Name < users[j].Name
+	})
+	return users, nil
+}
+
+//GetUser getting the usermodel
+func (m *MongoDAO) GetUser(username string) (model.User, bool) {
+	username = strings.ToLower(username)
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	usersCollection := m.database.Collection(usersCollectionName)
+	var user model.User
+	filter := bson.M{"name": username}
+	err := usersCollection.FindOne(ctx, filter).Decode(&user)
+	if err != nil {
+		fmt.Printf("error: %s\n", err.Error())
+		return model.User{}, false
+	}
+	password := user.Password
+	hash := BuildPasswordHash(password, user.Salt)
+	user.Password = hash
+	return user, true
+}
+
+// AddUser adding a new user to the system
+func (m *MongoDAO) AddUser(user model.User) (model.User, error) {
+	if user.Name == "" {
+		return model.User{}, errors.New("username should not be empty")
+	}
+	user.Name = strings.ToLower(user.Name)
+	_, ok := m.GetUser(user.Name)
+	if ok {
+		return model.User{}, errors.New("username already exists")
+	}
+
+	user.Salt, _ = crypt.GenerateRandomBytes(20)
+	user.Password = BuildPasswordHash(user.Password, user.Salt)
+
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collection := m.database.Collection(usersCollectionName)
+	_, err := collection.InsertOne(ctx, user)
+	if err != nil {
+		fmt.Printf("error: %s\n", err.Error())
+		return model.User{}, err
+	}
+	return user, nil
+}
+
+// DeleteUser deletes one user from the system
+func (m *MongoDAO) DeleteUser(username string) error {
+	if username == "" {
+		return errors.New("username should not be empty")
+	}
+	username = strings.ToLower(username)
+	_, ok := m.GetUser(username)
+	if !ok {
+		return errors.New("username not exists")
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collection := m.database.Collection(usersCollectionName)
+	filter := bson.M{"name": username}
+	_, err := collection.DeleteOne(ctx, filter)
+	if err != nil {
+		fmt.Printf("error: %s\n", err.Error())
+		return err
+	}
+	return nil
+}
+
+// ChangePWD changes the apssword of a single user
+func (m *MongoDAO) ChangePWD(username string, newpassword string) (model.User, error) {
+	if username == "" {
+		return model.User{}, errors.New("username should not be empty")
+	}
+	username = strings.ToLower(username)
+	userModel, ok := m.GetUser(username)
+	if !ok {
+		return model.User{}, errors.New("username not registered")
+	}
+
+	newsalt, _ := crypt.GenerateRandomBytes(20)
+	newpassword = BuildPasswordHash(newpassword, newsalt)
+	userModel.Password = newpassword
+	userModel.Salt = newsalt
+
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collection := m.database.Collection(usersCollectionName)
+	filter := bson.M{"name": username}
+	update := bson.D{{Key: "$set", Value: bson.D{{Key: "password", Value: newpassword}, {Key: "salt", Value: newsalt}}}}
+	result := collection.FindOneAndUpdate(ctx, filter, update)
+	if result.Err() != nil {
+		fmt.Printf("error: %s\n", result.Err().Error())
+		return model.User{}, result.Err()
+	}
+	return userModel, nil
+}
+
+//CreateModel creating a new model
+func (m *MongoDAO) CreateModel(route model.Route, data model.JSONMap) (string, error) {
+	collectionName := route.GetRouteName()
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collection := m.database.Collection(collectionName)
+	result, err := collection.InsertOne(ctx, data)
+	if err != nil {
+		switch v := err.(type) {
+		case mongo.WriteException:
+			if v.WriteErrors[0].Code == 11000 {
+				return "", ErrUniqueIndexError
+			}
+		}
+		fmt.Printf("error: %s\n", err.Error())
+		return "", err
+	}
+	switch v := result.InsertedID.(type) {
+	case primitive.ObjectID:
+		return v.Hex(), nil
+	}
+	return "", ErrUnknownError
+}
+
+//CreateModels creates a bunch of models
+func (m *MongoDAO) CreateModels(route model.Route, datas []model.JSONMap) ([]string, error) {
+	collectionName := route.GetRouteName()
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collection := m.database.Collection(collectionName)
+	models := make([]interface{}, 0)
+	for _, data := range datas {
+		models = append(models, data)
+	}
+	result, err := collection.InsertMany(ctx, models, &options.InsertManyOptions{})
+	if err != nil {
+		fmt.Printf("error: %s\n", err.Error())
+		return nil, err
+	}
+	modelids := make([]string, 0)
+	for _, id := range result.InsertedIDs {
+		switch v := id.(type) {
+		case primitive.ObjectID:
+			modelids = append(modelids, v.Hex())
+		}
+	}
+	return modelids, nil
+}
+
+//GetModel getting requested model from the storage
+func (m *MongoDAO) GetModel(route model.Route) (model.JSONMap, error) {
+	collectionName := route.GetRouteName()
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collection := m.database.Collection(collectionName)
+	objectID, _ := primitive.ObjectIDFromHex(route.Identity)
+	result := collection.FindOne(ctx, bson.M{"_id": objectID})
+	err := result.Err()
+	if err == mongo.ErrNoDocuments {
+		log.Alertf("%v", err)
+		return nil, ErrNoDocument
+	}
+	if err != nil {
+		log.Alertf("%v", err)
+		return nil, err
+	}
+	var bemodel model.JSONMap
+	if err := result.Decode(&bemodel); err != nil {
+		log.Alertf("%v", err)
+		return nil, err
+	}
+	//		bemodel[internal.AttributeID] = bemodel[internal.AttributeID].(primitive.ObjectID).Hex()
+	bemodel, _ = m.convertModel(bemodel)
+	return bemodel, nil
+}
+
+//CountModel counting all medelsin this collection
+func (m *MongoDAO) CountModel(route model.Route) (int, error) {
+	collectionName := route.GetRouteName()
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collection := m.database.Collection(collectionName)
+	n, err := collection.CountDocuments(ctx, bson.M{}, &options.CountOptions{})
+	if err == mongo.ErrNoDocuments {
+		log.Alertf("%v", err)
+		return 0, ErrNoDocument
+	}
+	if err != nil {
+		log.Alertf("%v", err)
+		return 0, err
+	}
+	return int(n), nil
+}
+
+//QueryModel query for the right models
+func (m *MongoDAO) QueryModel(route model.Route, query string, offset int, limit int) (int, []model.JSONMap, error) {
+	collectionName := route.GetRouteName()
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collection := m.database.Collection(collectionName)
+
+	var queryM map[string]interface{}
+	if query == "" {
+		queryM = make(map[string]interface{})
+	} else {
+		err := json.Unmarshal([]byte(query), &queryM)
+		if err != nil {
+			log.Alertf("%v", err)
+			return 0, nil, err
+		}
+	}
+
+	queryDoc := bson.M{}
+	for k, v := range queryM {
+		if k == "$fulltext" {
+			queryDoc["$text"] = bson.M{"$search": v}
+		} else {
+			switch v := v.(type) {
+			//			case float64:
+			//			case int:
+			//			case bool:
+			case string:
+				queryDoc[k] = bson.M{"$regex": v}
+			}
+			//queryDoc[k] = v
+		}
+	}
+	//data, _ := json.Marshal(queryDoc)
+	//log.Infof("mongoquery: %s", string(data))
+	n, err := collection.CountDocuments(ctx, queryDoc, &options.CountOptions{Collation: &options.Collation{Locale: "en", Strength: 2}})
+	if err != nil {
+		log.Alertf("%v", err)
+		return 0, nil, err
+	}
+	cursor, err := collection.Find(ctx, queryDoc, &options.FindOptions{Collation: &options.Collation{Locale: "en", Strength: 2}})
+	if err != nil {
+		log.Alertf("%v", err)
+		return 0, nil, err
+	}
+	defer cursor.Close(ctx)
+	models := make([]model.JSONMap, 0)
+	count := 0
+	docs := 0
+	for cursor.Next(ctx) {
+		if count >= offset {
+			if (docs < limit) || (limit <= 0) {
+				var model model.JSONMap
+				if err = cursor.Decode(&model); err != nil {
+					log.Alertf("%v", err)
+					return 0, nil, err
+				}
+				models = append(models, model)
+				docs++
+			} else {
+				break
+			}
+		}
+		count++
+	}
+	return int(n), models, nil
+}
+
+//UpdateModel updateing an existing datamodel in the mongo db
+func (m *MongoDAO) UpdateModel(route model.Route, data model.JSONMap) (model.JSONMap, error) {
+	collectionName := route.GetRouteName()
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collection := m.database.Collection(collectionName)
+	objectID, _ := primitive.ObjectIDFromHex(route.Identity)
+	delete(data, internal.AttributeID)
+
+	filter := bson.M{internal.AttributeID: objectID}
+	updateResult, err := collection.ReplaceOne(ctx, filter, data)
+	if err != nil {
+		return nil, err
+	}
+	if updateResult.ModifiedCount == 0 {
+		return nil, ErrUnknownError
+	}
+	newModel, err := m.GetModel(route)
+	if err != nil {
+		return nil, err
+	}
+	return newModel, nil
+}
+
+//DeleteModel deleting the requested model from the storage
+func (m *MongoDAO) DeleteModel(route model.Route) error {
+	collectionName := route.GetRouteName()
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collection := m.database.Collection(collectionName)
+	objectID, _ := primitive.ObjectIDFromHex(route.Identity)
+
+	filter := bson.M{internal.AttributeID: objectID}
+	result, err := collection.DeleteOne(ctx, filter)
+	if err != nil {
+		return err
+	}
+	if result.DeletedCount != 1 {
+		return ErrUnknownError
+	}
+	return nil
+}
+
+// GetIndexNames getting a list of index names
+func (m *MongoDAO) GetIndexNames(route model.Route) ([]string, error) {
+	collection := m.database.Collection(route.GetRouteName())
+	indexView := collection.Indexes()
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	cursor, err := indexView.List(ctx)
+	if err != nil {
+		log.Alertf("%v", err)
+		return nil, err
+	}
+	defer cursor.Close(ctx)
+	myIndexes := make([]string, 0)
+	for cursor.Next(ctx) {
+		var index bson.M
+		if err = cursor.Decode(&index); err != nil {
+			log.Alertf("%v", err)
+			return nil, err
+		}
+		name := index["name"].(string)
+		if !strings.HasPrefix(name, "_") {
+			if name == "$text" {
+				name = FulltextIndexName
+			}
+			myIndexes = append(myIndexes, name)
+		}
+	}
+
+	return myIndexes, nil
+}
+
+// DeleteIndex delete one search index
+func (m *MongoDAO) DeleteIndex(route model.Route, name string) error {
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collection := m.database.Collection(route.GetRouteName())
+	_, err := collection.Indexes().DropOne(ctx, name)
+	if err != nil {
+		log.Alertf("%v", err)
+		return err
+	}
+	return nil
+}
+
+//UpdateIndex create or update an index
+func (m *MongoDAO) UpdateIndex(route model.Route, index model.Index) error {
+	myIndexes, err := m.GetIndexNames(route)
+	if err != nil {
+		log.Alertf("%v", err)
+		return err
+	}
+
+	collection := m.database.Collection(route.GetRouteName())
+	indexView := collection.Indexes()
+
+	if !slicesutils.Contains(myIndexes, index.Name) {
+		var indexmodel mongo.IndexModel
+		if index.Name == FulltextIndexName {
+			keys := bson.D{}
+			for _, field := range index.Fields {
+				//TODO here must be implemented the right field type
+				keys = append(keys, primitive.E{
+					Key:   field,
+					Value: "text",
+				})
+			}
+			indexmodel = mongo.IndexModel{
+				Keys:    keys,
+				Options: options.Index().SetName("$text"),
+			}
+		} else {
+			keys := bson.D{}
+			for _, field := range index.Fields {
+				keys = append(keys, primitive.E{
+					Key:   field,
+					Value: 1,
+				})
+			}
+			// TODO here must be implemented the right language
+			idxOptions := options.Index().SetName(index.Name).SetCollation(&options.Collation{Locale: "en", Strength: 2})
+			if index.Unique {
+				idxOptions = idxOptions.SetUnique(true)
+			}
+			indexmodel = mongo.IndexModel{
+				Keys:    keys,
+				Options: idxOptions,
+			}
+		}
+
+		// Specify the MaxTime option to limit the amount of time the operation can run on the server
+		opts := options.CreateIndexes().SetMaxTime(2 * time.Second)
+		name, err := indexView.CreateOne(context.TODO(), indexmodel, opts)
+		if err != nil {
+			log.Alertf("%v", err)
+			return err
+		}
+		log.Infof("Index %s for route %s created.", name, route.GetRouteName())
+	}
+	return nil
+}
+
+// Ping pinging the mongoDao
+func (m *MongoDAO) Ping() error {
+	if !m.initialised {
+		return errors.New("mongo client not initialised")
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	return m.database.Client().Ping(ctx, nil)
+}
+
+// DeleteBackend dropping all data from the backend
+func (m *MongoDAO) DeleteBackend(backend string) error {
+	if backend == attachmentsCollectionName || backend == usersCollectionName {
+		return errors.New("wrong backend name")
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collectionNames, err := m.database.ListCollectionNames(ctx, bson.D{}, &options.ListCollectionsOptions{})
+	if err != nil {
+		log.Alertf("%v", err)
+		return err
+	}
+	for _, name := range collectionNames {
+		if strings.HasPrefix(name, backend+".") {
+			collection := m.database.Collection(name)
+			err = collection.Drop(ctx)
+			if err != nil {
+				log.Alertf("%v", err)
+				return err
+			}
+		}
+	}
+
+	filter := bson.M{"metadata.backend": backend}
+	cursor, err := m.bucket.Find(filter, &options.GridFSFindOptions{})
+	if err != nil {
+		log.Alertf("%v", err)
+		return err
+	}
+	defer cursor.Close(ctx)
+
+	for cursor.Next(ctx) {
+		var file bson.M
+		if err = cursor.Decode(&file); err != nil {
+			log.Alertf("%v", err)
+			return err
+		}
+		if err = m.bucket.Delete(file["_id"]); err != nil {
+			log.Alertf("%v", err)
+			return err
+		}
+	}
+	return nil
+}
+
+// DropAll dropping all data from the database
+func (m *MongoDAO) DropAll() {
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	collectionNames, err := m.database.ListCollectionNames(ctx, bson.D{}, &options.ListCollectionsOptions{})
+	if err != nil {
+		log.Alertf("%v", err)
+		return
+	}
+	for _, name := range collectionNames {
+		if name != usersCollectionName {
+			collection := m.database.Collection(name)
+			err = collection.Drop(ctx)
+			if err != nil {
+				log.Alertf("%v", err)
+				return
+			}
+		}
+	}
+}
+
+// Stop stopping the mongodao
+func (m *MongoDAO) Stop() {
+	m.ticker.Stop()
+	m.done <- true
+}
+
+func (m *MongoDAO) convertModel(srcModel model.JSONMap) (model.JSONMap, error) {
+	dstModel := srcModel
+	for k, v := range srcModel {
+		dstModel[k] = m.convertValue(v)
+	}
+	return dstModel, nil
+}
+
+func (m *MongoDAO) convertValue(value interface{}) interface{} {
+	switch v := value.(type) {
+	case primitive.ObjectID:
+		return v.Hex()
+	case primitive.A:
+		items := make([]interface{}, 0)
+		for _, itemValue := range v {
+			items = append(items, m.convertValue(itemValue))
+		}
+		return items
+	}
+	return value
+}

+ 57 - 0
MessageService/dao/storageDao.go

@@ -0,0 +1,57 @@
+package dao
+
+import (
+	"io"
+
+	"github.com/willie68/AutoRestIoT/model"
+)
+
+//FulltextIndexName name of the index containing fulltext data
+const FulltextIndexName = "$fulltext"
+
+/*
+StorageDao this is the interface which all method implementation of a storage engine has to fulfill
+*/
+type StorageDao interface {
+	ProcessFiles(RemoveCallback func(file model.FileInfo) bool) error
+	AddFile(backend string, filename string, reader io.Reader) (string, error)
+	GetFilename(backend string, fileid string) (string, error)
+	GetFile(backend string, fileid string, stream io.Writer) error
+	DeleteFile(backend string, fileid string) error
+
+	CreateModel(route model.Route, data model.JSONMap) (string, error)
+	CreateModels(route model.Route, datas []model.JSONMap) ([]string, error)
+	CountModel(route model.Route) (int, error)
+	GetModel(route model.Route) (model.JSONMap, error)
+	QueryModel(route model.Route, query string, offset int, limit int) (int, []model.JSONMap, error)
+	UpdateModel(route model.Route, data model.JSONMap) (model.JSONMap, error)
+	DeleteModel(route model.Route) error
+
+	GetIndexNames(route model.Route) ([]string, error)
+	DeleteIndex(route model.Route, name string) error
+	UpdateIndex(route model.Route, index model.Index) error
+
+	GetUsers() ([]model.User, error)
+	GetUser(username string) (model.User, bool)
+	AddUser(user model.User) (model.User, error)
+	DeleteUser(username string) error
+	ChangePWD(username string, newpassword string) (model.User, error)
+
+	DeleteBackend(beackend string) error
+	DropAll()
+	Ping() error
+
+	Stop()
+}
+
+var storageDao StorageDao
+
+//GetStorage getting the actual storage dao
+func GetStorage() StorageDao {
+	return storageDao
+}
+
+//SetStorage setting the actual storage dad
+func SetStorage(storage StorageDao) {
+	storageDao = storage
+}

+ 16 - 0
MessageService/devdata/mongo.txt

@@ -0,0 +1,16 @@
+use backend1
+db.createUser({ user: "backend1", pwd: "backend1", roles: [ "readWrite", "dbAdmin", { role: "dbOwner", db: "backend1" } ]})
+
+db.runCommand({compact: "attachments.files"})
+db.runCommand({compact: "attachments.chunks"})
+
+use admin
+
+query with Collation {locale: "en", strength : 2}
+
+{$or: [{$text : {$search : "blue"}} , { manufacturer: "swr"}, {model: "OM"}]}
+
+https://127.0.0.1:9443/api/v1/schematics?query={"$fulltext":"\"blue \" \"Mesa\""}&offset=0&limit=10
+
+schematic access
+{"name": "w.klaas@gmx.de", "password":"akteon0000", "admin": true}

+ 44 - 0
MessageService/devdata/notice.txt

@@ -0,0 +1,44 @@
+
+application := models.Application{
+		ApplicationName: "hallo",
+		Models: []models.BEModel{
+			models.BEModel{
+				Name: "test",
+				Fields: []models.Field{
+					models.Field{
+						Name:       "manufacturer",
+						Type:       "string",
+						Mandatory: true,
+						Collection: false,
+					},
+					models.Field{
+						Name:       "model",
+						Type:       "string",
+						Mandatory: true,
+						Collection: false,
+					},
+					models.Field{
+						Name:       "tags",
+						Type:       "string",
+						Mandatory: false,
+						Collection: true,
+					},
+				},
+				Indexes: []models.Index{
+					models.Index{
+						Name:   "fulltest",
+						Fields: []string{"manufacturer", "model", "tags"},
+					},
+					models.Index{
+						Name:   "manufacturer",
+						Fields: []string{"manufacturer"},
+					},
+					models.Index{
+						Name:   "tags",
+						Fields: []string{"tags"},
+					},
+				},
+			},
+		},
+	}
+    

+ 33 - 0
MessageService/go.mod

@@ -0,0 +1,33 @@
+module wkla.no-ip.biz/gogs/Willie/MsgService/
+
+go 1.14
+
+require (
+	github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a
+	github.com/aphistic/sweet v0.3.0 // indirect
+	github.com/eclipse/paho.mqtt.golang v1.2.0
+	github.com/go-chi/chi v4.1.1+incompatible
+	github.com/go-chi/docgen v1.0.5
+	github.com/go-chi/render v1.0.1
+	github.com/go-delve/delve v1.4.0 // indirect
+	github.com/gofrs/uuid v3.2.0+incompatible // indirect
+	github.com/goiiot/libmqtt v0.9.5 // indirect
+	github.com/google/uuid v1.1.1 // indirect
+	github.com/hashicorp/consul v1.7.2 // indirect
+	github.com/hashicorp/consul/api v1.4.0
+	github.com/joyent/triton-go v0.0.0-20180628001255-830d2b111e62
+	github.com/prometheus/client_golang v1.5.1 // indirect
+	github.com/qntfy/jsonparser v1.0.2 // indirect
+	github.com/qntfy/kazaam v3.4.8+incompatible
+	github.com/rs/zerolog v1.18.0 // indirect
+	github.com/spf13/pflag v1.0.5
+	github.com/spf13/viper v1.6.3 // indirect
+	github.com/stretchr/testify v1.5.1
+	github.com/willie68/kazaam v3.4.8+incompatible // indirect
+	go.mongodb.org/mongo-driver v1.3.2
+	golang.org/x/crypto v0.0.0-20191106202628-ed6320f186d4
+	golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
+	gopkg.in/square/go-jose.v2 v2.5.0 // indirect
+	gopkg.in/yaml.v2 v2.2.8
+	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
+)

+ 568 - 0
MessageService/go.sum

@@ -0,0 +1,568 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/Azure/azure-sdk-for-go v16.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-autorest v10.7.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest v10.15.3+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/Microsoft/go-winio v0.4.3/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/NYTimes/gziphandler v1.0.1/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a h1:2KLQMJ8msqoPHIPDufkxVcoTtcmE5+1sL9950m4R9Pk=
+github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
+github.com/aphistic/sweet v0.3.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=
+github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aws/aws-sdk-go v1.25.41/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
+github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coredns/coredns v1.1.2/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
+github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denverdino/aliyungo v0.0.0-20170926055100-d3308649c661/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/digitalocean/godo v1.1.1/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU=
+github.com/digitalocean/godo v1.10.0/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU=
+github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
+github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0=
+github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
+github.com/elazarl/go-bindata-assetfs v0.0.0-20160803192304-e1a2a7ec64b0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
+github.com/envoyproxy/go-control-plane v0.8.0/go.mod h1:GSSbY9P1neVhdY7G4wu+IK1rk/dqhiCC/4ExuWJZVuk=
+github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY=
+github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/go-chi/chi v4.1.1+incompatible h1:MmTgB0R8Bt/jccxp+t6S/1VGIKdJw5J74CK/c9tTfA4=
+github.com/go-chi/chi v4.1.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/go-chi/docgen v1.0.5 h1:TiGvJAuVPZJ9zFSwoF52eORe0SztOYqf9C79LVw/xbY=
+github.com/go-chi/docgen v1.0.5/go.mod h1:Nm4H4RaynSlvTexxWYWwXBzrwZKRE00MrkIIcJelhWM=
+github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
+github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
+github.com/go-delve/delve v1.4.0/go.mod h1:gQM0ReOJLNAvPuKAXfjHngtE93C2yc/ekTbo7YbAHSo=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
+github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
+github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
+github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
+github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
+github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
+github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
+github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
+github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
+github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
+github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
+github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
+github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
+github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
+github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
+github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
+github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
+github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
+github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
+github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
+github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
+github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
+github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
+github.com/gofrs/uuid v1.2.0 h1:coDhrjgyJaglxSjxuJdqQSSdUpG3w6p1OwN2od6frBU=
+github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
+github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/goiiot/libmqtt v0.9.5 h1:OmD18y0mfFSFaGe2IM38ku6FdAlbpTfe7lPZsmBeqRM=
+github.com/goiiot/libmqtt v0.9.5/go.mod h1:ppDqp5KgQme3Ngedfeg956rBwjphWOYJraz1ScG01tI=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
+github.com/gophercloud/gophercloud v0.0.0-20180828235145-f29afc2cceca/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
+github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/consul v1.7.1 h1:HgnJOJWGc8PIqRYa5VKT3KXB5fqYqloX/u5Bk1bY3/8=
+github.com/hashicorp/consul v1.7.1/go.mod h1:vKfXmSQNl6HwO/JqQ2DDLzisBDV49y+JVTkrdW1cnSU=
+github.com/hashicorp/consul v1.7.2 h1:pDEnRiUE8jOUlxIqzo8Jw3Zcsz6KSpygk2BjkrsASsk=
+github.com/hashicorp/consul v1.7.2/go.mod h1:vKfXmSQNl6HwO/JqQ2DDLzisBDV49y+JVTkrdW1cnSU=
+github.com/hashicorp/consul/api v1.4.0 h1:jfESivXnO5uLdH650JU/6AnjRoHrLhULq0FnC3Kp9EY=
+github.com/hashicorp/consul/api v1.4.0/go.mod h1:xc8u05kyMa3Wjr9eEAsIAo3dg8+LywT5E/Cl7cNS5nU=
+github.com/hashicorp/consul/sdk v0.4.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-bexpr v0.1.2/go.mod h1:ANbpTX1oAql27TZkKVeW8p1w8NTdnyzPe/0qqPCKohU=
+github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de/go.mod h1:xIwEieBHERyEvaeKF/TcHh1Hu+lxPM+n2vT1+g9I4m4=
+github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-connlimit v0.2.0/go.mod h1:OUj9FGL1tPIhl/2RCfzYHrIiWj+VVPGNyVPnUX8AqS0=
+github.com/hashicorp/go-discover v0.0.0-20191202160150-7ec2cfbda7a2/go.mod h1:NnH5X4UCBEBdTuK2L8s4e4ilJm3UmGX0bANHCz0HSs0=
+github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
+github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-hclog v0.12.0 h1:d4QkX8FRTYaKaCZBoXYY8zJX2BXjWxurN/GA2tkrmZM=
+github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc=
+github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-memdb v1.0.3/go.mod h1:LWQ8R70vPrS4OEY9k28D2z8/Zzyu34NVzeRibGAzHO0=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
+github.com/hashicorp/go-raftchunking v0.6.1/go.mod h1:cGlg3JtDy7qy6c/3Bu660Mic1JF+7lWqIwCFSb08fX0=
+github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/hil v0.0.0-20160711231837-1e86c6b523c5/go.mod h1:KHvg/R2/dPtaePb16oW4qIyzkMxXOL38xjRN64adsts=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/memberlist v0.1.6/go.mod h1:5VDNHjqFMgEcclnwmkCnC99IPwxBmIsxwY8qn+Nl0H4=
+github.com/hashicorp/net-rpc-msgpackrpc v0.0.0-20151116020338-a14192a58a69/go.mod h1:/z+jUGRBlwVpUZfjute9jWaF6/HuhjuFQuL1YXzVD1Q=
+github.com/hashicorp/raft v1.1.1/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8=
+github.com/hashicorp/raft v1.1.2/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8=
+github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hashicorp/serf v0.8.5 h1:ZynDUIQiA8usmRgPdGPHFdPnb1wgGI9tK3mO9hcAJjc=
+github.com/hashicorp/serf v0.8.5/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k=
+github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
+github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
+github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443/go.mod h1:bEpDU35nTu0ey1EXjwNwPjI9xErAsoOCmcMb9GKvyxo=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
+github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/joyent/triton-go v0.0.0-20180628001255-830d2b111e62 h1:JHCT6xuyPUrbbgAPE/3dqlvUKzRHMNuTBKKUb6OeR/k=
+github.com/joyent/triton-go v0.0.0-20180628001255-830d2b111e62/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA=
+github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
+github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.9.5 h1:U+CaK85mrNNb4k8BNOfgJtJ/gr6kswUCFj6miSzVC6M=
+github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/likexian/gokit v0.0.0-20190309162924-0a377eecf7aa/go.mod h1:QdfYv6y6qPA9pbBA2qXtoT8BMKha6UyNbxWGWl/9Jfk=
+github.com/likexian/gokit v0.0.0-20190418170008-ace88ad0983b/go.mod h1:KKqSnk/VVSW8kEyO2vVCXoanzEutKdlBAPohmGXkxCk=
+github.com/likexian/gokit v0.0.0-20190501133040-e77ea8b19cdc/go.mod h1:3kvONayqCaj+UgrRZGpgfXzHdMYCAO0KAt4/8n0L57Y=
+github.com/likexian/gokit v0.20.16/go.mod h1:kn+nTv3tqh6yhor9BC4Lfiu58SmH8NmQ2PmEl+uM6nU=
+github.com/likexian/simplejson-go v0.0.0-20190409170913-40473a74d76d/go.mod h1:Typ1BfnATYtZ/+/shXfFYLrovhFyuKvzwrdOnIDHlmg=
+github.com/likexian/simplejson-go v0.0.0-20190419151922-c1f9f0b4f084/go.mod h1:U4O1vIJvIKwbMZKUJ62lppfdvkCdVd2nfMimHK81eec=
+github.com/likexian/simplejson-go v0.0.0-20190502021454-d8787b4bfa0b/go.mod h1:3BWwtmKP9cXWwYCr5bkoVDEfLywacOv0s06OBEDpyt8=
+github.com/linode/linodego v0.7.1/go.mod h1:ga11n3ivecUrPCHN0rANxKmfWBJVkOXfLMZinAbj2sY=
+github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
+github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
+github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
+github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/hashstructure v0.0.0-20170609045927-2bca23e0e452/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2/go.mod h1:TLb2Sg7HQcgGdloNxkrmtgDNR9uVYF3lfdFIN4Ro6Sk=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c/go.mod h1:otzZQXgoO96RTzDB/Hycg0qZcXZsWJGJRSXbmEIJ+4M=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
+github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/profile v0.0.0-20170413231811-06b906832ed0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
+github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/qntfy/jsonparser v1.0.2 h1:hko+J4L7HSaYoB2yuzinWc9MkO93zWKUmzPHJwB53OM=
+github.com/qntfy/jsonparser v1.0.2/go.mod h1:F+LCdwPnFBsubQ+ugnBczIP9RWv5wSCqnUmLHPUx4ZU=
+github.com/qntfy/kazaam v1.3.0 h1:oATJJN5WHqYbfmhLf1hCzTONeCia15j9rs+8w5/kVoQ=
+github.com/qntfy/kazaam v3.4.8+incompatible h1:UB8fXIel0m+YCEPYi7KK+5jjbVLTkNNL632/y7PAC2c=
+github.com/qntfy/kazaam v3.4.8+incompatible/go.mod h1:aN8m9eOLEtyeypys9YtGYm0rFjKWlobu18ez6GcBtsg=
+github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03/go.mod h1:gRAiPF5C5Nd0eyyRdqIu9qTiFSoZzpTq727b5B8fkkU=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
+github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
+github.com/russross/blackfriday v0.0.0-20180428102519-11635eb403ff/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/shirou/gopsutil v0.0.0-20181107111621-48177ef5f880/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
+github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d/go.mod h1:Cw4GTlQccdRGSEf6KiMju767x0NEHE0YIVPJSaXjlsw=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
+github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/tencentcloud/tencentcloud-sdk-go v3.0.83+incompatible/go.mod h1:0PfYow01SHPMhKY31xa+EFz2RStxIqj6JFAJS+IkCi4=
+github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9/go.mod h1:RHkNRtSLfOK7qBTHaeSX1D6BNpI3qw7NTxsmNr4RvN8=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
+github.com/vmware/govmomi v0.18.0/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=
+github.com/willie68/kazaam v1.3.0 h1:E1eLflY6qEoZ1EnT4Wp5DHfoZSRTS8glKhoZpVPezng=
+github.com/willie68/kazaam v3.4.8+incompatible h1:yl2+f1Ek1MqvygzyJ9rK+DU3O2GlcQubYp5en0MFiBg=
+github.com/willie68/kazaam v3.4.8+incompatible/go.mod h1:6E4t9I22indp4Wbsj6GmzWY1rIhm9YRHS5ZPfcIjknU=
+github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
+github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
+github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc h1:n+nNi93yXLkJvKwXNP9d55HC7lGK4H/SRcwB5IaUZLo=
+github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.mongodb.org/mongo-driver v1.3.1 h1:op56IfTQiaY2679w922KVWa3qcHdml2K/Io8ayAOUEQ=
+go.mongodb.org/mongo-driver v1.3.1/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE=
+go.mongodb.org/mongo-driver v1.3.2 h1:IYppNjEV/C+/3VPbhHVxQ4t04eVW0cLp0/pNdW++6Ug=
+go.mongodb.org/mongo-driver v1.3.2/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE=
+go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
+golang.org/x/crypto v0.0.0-20191106202628-ed6320f186d4 h1:PDpCLFAH/YIX0QpHPf2eO7L4rC2OOirBrKtXTLLiNTY=
+golang.org/x/crypto v0.0.0-20191106202628-ed6320f186d4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/oauth2 v0.0.0-20170807180024-9a379c6b3e95/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190508220229-2d0786266e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.0.0-20180829000535-087779f1d2c9/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
+gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
+gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71 h1:Xe2gvTZUJpsvOWUnvmL/tmhVBZUmHSvLbMjRj6NUUKo=
+gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+istio.io/gogo-genproto v0.0.0-20190124151557-6d926a6e6feb/go.mod h1:eIDJ6jNk/IeJz6ODSksHl5Aiczy5JUq6vFhJWI5OtiI=
+k8s.io/api v0.0.0-20180806132203-61b11ee65332/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
+k8s.io/api v0.0.0-20190325185214-7544f9db76f6/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
+k8s.io/apimachinery v0.0.0-20180821005732-488889b0007f/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
+k8s.io/apimachinery v0.0.0-20190223001710-c182ff3b9841/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
+k8s.io/client-go v8.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
+launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM=
+nhooyr.io/websocket v1.7.4 h1:w/LGB2sZT0RV8lZYR7nfyaYz4PUbYZ5oF7NBon2M0NY=
+nhooyr.io/websocket v1.7.4/go.mod h1:PxYxCwFdFYQ0yRvtQz3s/dC+VEm7CSuC/4b9t8MQQxw=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

+ 109 - 0
MessageService/health/health.go

@@ -0,0 +1,109 @@
+package health
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/go-chi/chi"
+	"github.com/willie68/AutoRestIoT/dao"
+	"github.com/willie68/AutoRestIoT/logging"
+)
+
+// global storage definition
+var myhealthy bool
+var log logging.ServiceLogger
+
+/*
+This is the healtchcheck you will have to provide.
+*/
+func check() (bool, string) {
+	myhealthy = true
+	message := ""
+	err := dao.GetStorage().Ping()
+	if err != nil {
+		myhealthy = false
+		message = err.Error()
+	}
+	return myhealthy, message
+}
+
+//##### template internal functions for processing the healthchecks #####
+var healthmessage string
+var healthy bool
+var lastChecked time.Time
+var period int
+
+// CheckConfig configuration for the healthcheck system
+type CheckConfig struct {
+	Period int
+}
+
+// InitHealthSystem initialise the complete health system
+func InitHealthSystem(config CheckConfig) {
+	period = config.Period
+	log.Infof("healthcheck starting with period: %d seconds", period)
+	healthmessage = "service starting"
+	healthy = false
+	doCheck()
+	go func() {
+		background := time.NewTicker(time.Second * time.Duration(period))
+		for range background.C {
+			doCheck()
+		}
+	}()
+}
+
+/*
+internal function to process the health check
+*/
+func doCheck() {
+	var msg string
+	healthy, msg = check()
+	if !healthy {
+		healthmessage = msg
+	} else {
+		healthmessage = ""
+	}
+	lastChecked = time.Now()
+}
+
+/*
+Routes getting all routes for the health endpoint
+*/
+func Routes() *chi.Mux {
+	router := chi.NewRouter()
+	router.Get("/health", GetHealthyEndpoint)
+	router.Get("/readiness", GetReadinessEndpoint)
+	return router
+}
+
+/*
+GetHealthyEndpoint is this service healthy
+*/
+func GetHealthyEndpoint(response http.ResponseWriter, req *http.Request) {
+	t := time.Now()
+	if t.Sub(lastChecked) > (time.Second * time.Duration(2*period)) {
+		healthy = false
+		healthmessage = "Healthcheck not running"
+	}
+	response.Header().Add("Content-Type", "application/json")
+	if healthy {
+		response.WriteHeader(http.StatusOK)
+		message := fmt.Sprintf(`{ "message": "service up and running", "lastCheck": "%s" }`, lastChecked.String())
+		response.Write([]byte(message))
+	} else {
+		response.WriteHeader(http.StatusServiceUnavailable)
+		message := fmt.Sprintf(`{ "message": "service is unavailable: %s", "lastCheck": "%s" }`, healthmessage, lastChecked.String())
+		response.Write([]byte(message))
+	}
+}
+
+/*
+GetReadinessEndpoint is this service ready for taking requests
+*/
+func GetReadinessEndpoint(response http.ResponseWriter, req *http.Request) {
+	response.Header().Add("Content-Type", "application/json")
+	response.WriteHeader(http.StatusOK)
+	response.Write([]byte(`{ "message": "service started" }`))
+}

+ 140 - 0
MessageService/internal/crypt/generatecert.go

@@ -0,0 +1,140 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+// Generate a self-signed X.509 certificate for a TLS server. Outputs to
+// 'cert.pem' and 'key.pem' and will overwrite existing files.
+package crypt
+
+import (
+	"crypto/ecdsa"
+	"crypto/ed25519"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"log"
+	"math/big"
+	"net"
+	"strings"
+	"time"
+)
+
+type GenerateCertificate struct {
+	Organization string
+	Host         string
+	ValidFrom    string
+	ValidFor     time.Duration
+	IsCA         bool
+	RSABits      int
+	EcdsaCurve   string
+	Ed25519Key   bool
+}
+
+func (gc *GenerateCertificate) publicKey(priv interface{}) interface{} {
+	switch k := priv.(type) {
+	case *rsa.PrivateKey:
+		return &k.PublicKey
+	case *ecdsa.PrivateKey:
+		return &k.PublicKey
+	case ed25519.PrivateKey:
+		return k.Public().(ed25519.PublicKey)
+	default:
+		return nil
+	}
+}
+
+func (gc *GenerateCertificate) GenerateTLSConfig() (*tls.Config, error) {
+	var priv interface{}
+	var err error
+	switch gc.EcdsaCurve {
+	case "":
+		if gc.Ed25519Key {
+			_, priv, err = ed25519.GenerateKey(rand.Reader)
+		} else {
+			priv, err = rsa.GenerateKey(rand.Reader, gc.RSABits)
+		}
+	case "P224":
+		priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
+	case "P256":
+		priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	case "P384":
+		priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+	case "P521":
+		priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+	default:
+		log.Fatalf("Unrecognized elliptic curve: %q", gc.EcdsaCurve)
+		return nil, err
+	}
+	if err != nil {
+		log.Fatalf("Failed to generate private key: %v", err)
+		return nil, err
+	}
+
+	var notBefore time.Time
+	if len(gc.ValidFrom) == 0 {
+		notBefore = time.Now()
+	} else {
+		notBefore, err = time.Parse("Jan 2 15:04:05 2006", gc.ValidFrom)
+		if err != nil {
+			log.Fatalf("Failed to parse creation date: %v", err)
+			return nil, err
+		}
+	}
+
+	notAfter := notBefore.Add(gc.ValidFor)
+
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		log.Fatalf("Failed to generate serial number: %v", err)
+		return nil, err
+	}
+
+	template := x509.Certificate{
+		SerialNumber: serialNumber,
+		Subject: pkix.Name{
+			Organization: []string{gc.Organization},
+		},
+		NotBefore: notBefore,
+		NotAfter:  notAfter,
+
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
+	}
+
+	hosts := strings.Split(gc.Host, ",")
+	for _, h := range hosts {
+		if ip := net.ParseIP(h); ip != nil {
+			template.IPAddresses = append(template.IPAddresses, ip)
+		} else {
+			template.DNSNames = append(template.DNSNames, h)
+		}
+	}
+
+	if gc.IsCA {
+		template.IsCA = true
+		template.KeyUsage |= x509.KeyUsageCertSign
+	}
+
+	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, gc.publicKey(priv), priv)
+	if err != nil {
+		log.Fatalf("Failed to create certificate: %v", err)
+		return nil, err
+	}
+
+	privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
+	if err != nil {
+		log.Fatalf("Unable to marshal private key: %v", err)
+		return nil, err
+	}
+
+	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
+	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
+	tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
+
+	return &tls.Config{Certificates: []tls.Certificate{tlsCert}}, nil
+}

+ 31 - 0
MessageService/internal/crypt/salt.go

@@ -0,0 +1,31 @@
+package crypt
+
+import (
+	"crypto/rand"
+	"encoding/base64"
+)
+
+// GenerateRandomBytes returns securely generated random bytes.
+// It will return an error if the system's secure random
+// number generator fails to function correctly, in which
+// case the caller should not continue.
+func GenerateRandomBytes(n int) ([]byte, error) {
+	b := make([]byte, n)
+	_, err := rand.Read(b)
+	// Note that err == nil only if we read len(b) bytes.
+	if err != nil {
+		return nil, err
+	}
+
+	return b, nil
+}
+
+// GenerateRandomString returns a URL-safe, base64 encoded
+// securely generated random string.
+// It will return an error if the system's secure random
+// number generator fails to function correctly, in which
+// case the caller should not continue.
+func GenerateRandomString(s int) (string, error) {
+	b, err := GenerateRandomBytes(s)
+	return base64.URLEncoding.EncodeToString(b), err
+}

+ 45 - 0
MessageService/internal/slicesutils/slicesutils.go

@@ -0,0 +1,45 @@
+package slicesutils
+
+/*
+Contains checking if the e string is present in the slice s
+*/
+func Contains(s []string, e string) bool {
+	for _, a := range s {
+		if a == e {
+			return true
+		}
+	}
+	return false
+}
+
+/*
+Remove removes the entry with the index i from the slice
+*/
+func Remove(s []string, i int) []string {
+	s[i] = s[len(s)-1]
+	// We do not need to put s[i] at the end, as it will be discarded anyway
+	return s[:len(s)-1]
+}
+
+/*
+Remove removes the e entry from the s slice, if e is not present in the slice, nothing will happen
+*/
+func RemoveString(s []string, e string) []string {
+	index := Find(s, e)
+	if index >= 0 {
+		return Remove(s, index)
+	}
+	return s
+}
+
+/*
+Find finding the index of the e string in the s slice
+*/
+func Find(s []string, e string) int {
+	for i, n := range s {
+		if e == n {
+			return i
+		}
+	}
+	return -1
+}

+ 58 - 0
MessageService/internal/slicesutils/slicesutils_test.go

@@ -0,0 +1,58 @@
+package slicesutils_test
+
+import (
+	"testing"
+
+	"github.com/willie68/AutoRestIoT/internal/slicesutils"
+)
+
+func TestContains(t *testing.T) {
+	mySlice := []string{"Willie", "Arthur", "Till"}
+	value := slicesutils.Contains(mySlice, "Willie")
+	if value != true {
+		t.Errorf("Willie was not in the slice")
+	}
+}
+func TestRemoveString(t *testing.T) {
+	mySlice := []string{"Willie", "Arthur", "Till"}
+	value := slicesutils.RemoveString(mySlice, "Willie")
+	if slicesutils.Contains(value, "Willie") {
+		t.Errorf("Willie was not removed from the slice")
+	}
+	value = slicesutils.RemoveString(mySlice, "Herman")
+	if len(value) != 3 {
+		t.Errorf("slice not unchanged")
+	}
+}
+
+func TestRemove(t *testing.T) {
+	mySlice := []string{"Willie", "Arthur", "Till"}
+	value := slicesutils.Remove(mySlice, 0)
+	if slicesutils.Contains(value, "Willie") {
+		t.Errorf("Willie was not removed from the slice")
+	}
+}
+
+func TestFind(t *testing.T) {
+	mySlice := []string{"Willie", "Arthur", "Till"}
+	value := slicesutils.Find(mySlice, "Willie")
+	if value != 0 {
+		t.Errorf("Willie was not found in the slice: index: %d", value)
+	}
+	value = slicesutils.Find(mySlice, "Arthur")
+	if value != 1 {
+		t.Errorf("Arthur was not found in the slice: index: %d", value)
+	}
+	value = slicesutils.Find(mySlice, "Till")
+	if value != 2 {
+		t.Errorf("Till was not found in the slice: index: %d", value)
+	}
+	value = slicesutils.Find(mySlice, "till")
+	if value >= 0 {
+		t.Errorf("till was wrongly found in the slice: index: %d", value)
+	}
+	value = slicesutils.Find(mySlice, "Herman")
+	if value >= 0 {
+		t.Errorf("Herman was wrongly found in the slice: index: %d", value)
+	}
+}

+ 125 - 0
MessageService/logging/logging.go

@@ -0,0 +1,125 @@
+package logging
+
+import (
+	"fmt"
+	"log"
+
+	golf "github.com/aphistic/golf"
+)
+
+/*
+ServiceLogger main type for logging
+*/
+type ServiceLogger struct {
+	GelfURL    string
+	GelfPort   int
+	SystemID   string
+	Attrs      map[string]interface{}
+	gelfActive bool
+	c          *golf.Client
+}
+
+/*
+InitGelf initialise gelf logging
+*/
+func (s *ServiceLogger) InitGelf() {
+	s.gelfActive = false
+	if s.GelfURL != "" {
+		s.c, _ = golf.NewClient()
+		s.c.Dial(fmt.Sprintf("udp://%s:%d", s.GelfURL, s.GelfPort))
+
+		l, _ := s.c.NewLogger()
+
+		golf.DefaultLogger(l)
+		for key, value := range s.Attrs {
+			l.SetAttr(key, value)
+		}
+		l.SetAttr("system_id", s.SystemID)
+		s.gelfActive = true
+	}
+}
+
+/*
+Debug log this maeesage at debug level
+*/
+func (s *ServiceLogger) Debug(msg string) {
+	if s.gelfActive {
+		golf.Info(msg)
+	}
+	log.Println(msg)
+}
+
+/*
+Debugf log this maeesage at debug level with formatting
+*/
+func (s *ServiceLogger) Debugf(format string, va ...interface{}) {
+	if s.gelfActive {
+		golf.Infof(format, va...)
+	}
+	log.Printf(format+"\n", va...)
+}
+
+/*
+Info log this maeesage at info level
+*/
+func (s *ServiceLogger) Info(msg string) {
+	if s.gelfActive {
+		golf.Info(msg)
+	}
+	log.Println(msg)
+}
+
+/*
+Infof log this maeesage at info level with formatting
+*/
+func (s *ServiceLogger) Infof(format string, va ...interface{}) {
+	if s.gelfActive {
+		golf.Infof(format, va...)
+	}
+	log.Printf(format+"\n", va...)
+}
+
+/*
+Alert log this maeesage at alert level
+*/
+func (s *ServiceLogger) Alert(msg string) {
+	if s.gelfActive {
+		golf.Alert(msg)
+	}
+	log.Printf("Alert: %s\n", msg)
+}
+
+/*
+Alertf log this maeesage at alert level with formatting
+*/
+func (s *ServiceLogger) Alertf(format string, va ...interface{}) {
+	if s.gelfActive {
+		golf.Alertf(format, va...)
+	}
+	log.Printf("Alert: %s\n", fmt.Sprintf(format, va...))
+}
+
+// Fatal logs a message at level Fatal on the standard logger.
+func (s *ServiceLogger) Fatal(msg string) {
+	if s.gelfActive {
+		golf.Crit(msg)
+	}
+	log.Printf("Fatal: %s\n", msg)
+}
+
+// Fatalf logs a message at level Fatal on the standard logger.
+func (s *ServiceLogger) Fatalf(format string, va ...interface{}) {
+	if s.gelfActive {
+		golf.Critf(format, va...)
+	}
+	log.Printf("Fatal: %s\n", fmt.Sprintf(format, va...))
+}
+
+/*
+Close this logging client
+*/
+func (s *ServiceLogger) Close() {
+	if s.gelfActive {
+		s.c.Close()
+	}
+}

+ 14 - 0
MessageService/model/model.go

@@ -0,0 +1,14 @@
+package model
+
+import "time"
+
+//JSONMap structure for json objects
+type JSONMap map[string]interface{}
+
+//FileInfo type
+type FileInfo struct {
+	Filename   string
+	ID         string
+	Backend    string
+	UploadDate time.Time
+}

+ 14 - 0
MessageService/model/mqtt.go

@@ -0,0 +1,14 @@
+package model
+
+//DataSourceConfigMQTT definition of the special configuration of a mqtt datasource
+type DataSourceConfigMQTT struct {
+	Broker                   string `yaml:"broker" json:"broker"`
+	Topic                    string `yaml:"topic" json:"topic"`
+	QoS                      int    `yaml:"qos" json:"qos"`
+	Payload                  string `yaml:"payload" json:"payload"`
+	Username                 string `yaml:"username" json:"username"`
+	Password                 string `yaml:"password" json:"password"`
+	AddTopicAsAttribute      string `yaml:"addTopicAsAttribute" json:"addTopicAsAttribute"`
+	SimpleValueAttribute     string `yaml:"simpleValueAttribute" json:"simpleValueAttribute"`
+	SimpleValueAttributeType string `yaml:"simpleValueAttributeType" json:"simpleValueAttributeType"`
+}

+ 27 - 0
MessageService/model/route.go

@@ -0,0 +1,27 @@
+package model
+
+import "fmt"
+
+//Route defining the route to a model
+type Route struct {
+	Backend  string
+	Model    string
+	Identity string
+	SystemID string
+	Apikey   string
+	Username string
+}
+
+//GetRouteName getting the route name as backend.model
+func (r *Route) GetRouteName() string {
+	return fmt.Sprintf("%s.%s", r.Backend, r.Model)
+}
+
+//String getting the route and, if given, the identity as string
+func (r *Route) String() string {
+	route := fmt.Sprintf("%s.%s", r.Backend, r.Model)
+	if r.Identity != "" {
+		route = fmt.Sprintf("%s.%s", route, r.Identity)
+	}
+	return route
+}

+ 45 - 0
MessageService/model/task.go

@@ -0,0 +1,45 @@
+package model
+
+import (
+	"encoding/json"
+
+	"github.com/willie68/AutoRestIoT/logging"
+)
+
+const TaskOrphanedFilesReport = "orphanedFilesReport"
+const TaskOrphanedFilesDelete = "orphanedFilesDelete"
+
+type TaskStatus string
+
+const (
+	New      TaskStatus = "new"
+	Running             = "running"
+	Finished            = "finished"
+)
+
+type Task struct {
+	ID     string     `yaml:"id" json:"id"`
+	Name   string     `yaml:"name" json:"name"`
+	Type   string     `yaml:"ttype" json:"ttype"`
+	Status TaskStatus `yaml:"tstatus" json:"tstatus"`
+	File   string     `yaml:"tfile" json:"tfile"`
+	Data   JSONMap    `yaml:"tdata" json:"tdata"`
+}
+
+var log logging.ServiceLogger
+
+//ToJSONMap converting task to json map
+func (t *Task) ToJSONMap() (JSONMap, error) {
+	taskModel := JSONMap{}
+	jsonData, err := json.Marshal(t)
+	if err != nil {
+		log.Alertf("error: %v", err)
+		return nil, err
+	}
+	err = json.Unmarshal(jsonData, &taskModel)
+	if err != nil {
+		log.Alertf("error: %v", err)
+		return nil, err
+	}
+	return taskModel, nil
+}

+ 17 - 0
MessageService/model/user.go

@@ -0,0 +1,17 @@
+package model
+
+import "go.mongodb.org/mongo-driver/bson/primitive"
+
+//User the user model
+type User struct {
+	ID          primitive.ObjectID `json:"-" bson:"_id,omitempty"`
+	Name        string             `json:"name" bson:"name,omitempty"`
+	Firstname   string             `json:"firstname" bson:"firstname,omitempty"`
+	Lastname    string             `json:"lastname" bson:"lastname,omitempty"`
+	Salt        []byte             `json:"salt,omitempty" bson:"salt,omitempty"`
+	Password    string             `json:"password,omitempty" bson:"password,omitempty"`
+	NewPassword string             `json:"newpassword,omitempty" bson:"-"`
+	Admin       bool               `json:"admin" bson:"admin,omitempty"`
+	Guest       bool               `json:"guest" bson:"guest,omitempty"`
+	Roles       []string           `json:"roles" bson:"roles,omitempty"`
+}

+ 34 - 0
MessageService/scratch.Dockerfile

@@ -0,0 +1,34 @@
+##### BUILDER #####
+
+# there is no need to build everything again. we already have the alpine based image and can use its atifacts 
+
+##### TARGET #####
+ARG RELEASE
+
+FROM gomicro:alpine-${RELEASE} AS copy-src
+
+FROM scratch
+
+ARG RELEASE
+ENV IMG_VERSION="${RELEASE}"
+
+# hadolint ignore=DL3022
+COPY --from=copy-src /usr/local/bin/gomicro /
+
+# hadolint ignore=DL3022
+COPY --from=copy-src /config/service.yaml /config/
+
+ENTRYPOINT ["/gomicro"]
+CMD ["--config","/config/service.yaml"]
+
+EXPOSE 8080 8443
+
+LABEL org.opencontainers.image.title="GoMicro" \
+      org.opencontainers.image.description="DM GoMicro" \
+      org.opencontainers.image.version="${IMG_VERSION}" \
+      org.opencontainers.image.source="https://bitbucket.easy.de/scm/dm/service-gomicro-go.git" \
+      org.opencontainers.image.vendor="EASY SOFTWARE AG (www.easy-software.com)" \
+      org.opencontainers.image.authors="EASY Apiomat GmbH" \
+      maintainer="EASY Apiomat GmbH" \
+      NAME="gomicro"
+

+ 326 - 0
MessageService/worker/model.go

@@ -0,0 +1,326 @@
+package worker
+
+/*
+ the worker middleware is for doing some part of tranformtion on the business object side.
+ Here you will find functions for validating the model validating and storage and retrieval in an storage technologie indipendent way.
+*/
+
+import (
+	"errors"
+	"fmt"
+	"reflect"
+	"sync"
+	"time"
+
+	"github.com/willie68/AutoRestIoT/config"
+	"github.com/willie68/AutoRestIoT/dao"
+	"github.com/willie68/AutoRestIoT/internal"
+	"github.com/willie68/AutoRestIoT/logging"
+	"github.com/willie68/AutoRestIoT/model"
+)
+
+//ErrMissingID the id of the model is mandatory and not availble
+var ErrMissingID = errors.New("Missing _id")
+
+//ErrBackendNotFound backend with that name was not found
+var ErrBackendNotFound = errors.New("Missing backend")
+
+//ErrBackendModelNotFound backend model with that name was not found
+var ErrBackendModelNotFound = errors.New("Missing backend model")
+
+type ErrValidationError struct {
+	message string
+}
+
+func (p ErrValidationError) Error() string {
+	return p.message
+}
+
+var modelCount = make(map[string]int)
+var modelCountMutex = &sync.Mutex{}
+
+func GetModelCount() map[string]int {
+	internaleCountMap := make(map[string]int)
+	modelCountMutex.Lock()
+	for k, v := range modelCount {
+		internaleCountMap[k] = v
+	}
+	modelCountMutex.Unlock()
+	return internaleCountMap
+}
+
+func IncModelCounter(name string, count int) {
+	modelCountMutex.Lock()
+	modelCount[name] = modelCount[name] + count
+	modelCountMutex.Unlock()
+}
+
+var log logging.ServiceLogger
+
+//CheckRoute checking if the route inforamtion are ok
+func CheckRoute(route model.Route) error {
+	if !config.Get().AllowAnonymousBackend {
+		backend, ok := model.BackendList.Get(route.Backend)
+		if !ok {
+			log.Alertf("backend not found: %s", route.Backend)
+			return ErrBackendNotFound
+		}
+		_, ok = backend.GetModel(route.Model)
+		if !ok {
+			log.Alertf("backend model not found: %s.%s", route.Backend, route.Model)
+			return ErrBackendModelNotFound
+		}
+	}
+	return nil
+}
+
+//Validate validates the model against the definition, and convert attributes, if there is something to convert (like dateTime attributes)
+func Validate(route model.Route, data model.JSONMap) (model.JSONMap, error) {
+	// return false, dao.ErrNotImplemented
+	err := CheckRoute(route)
+	if err != nil {
+		return nil, err
+	}
+	modelDefinition, ok := model.BackendList.GetModel(route)
+	if !ok || !config.Get().AllowAnonymousBackend {
+		return nil, ErrBackendModelNotFound
+	}
+	log.Info(modelDefinition.Name)
+	// check fieldtypes, eventually convert
+	//TODO check field values
+	for key, value := range data {
+		field, ok := modelDefinition.GetField(key)
+		if !ok {
+			continue
+		}
+		if field.Mandatory {
+			if isEmpty(value) {
+				return nil, ErrValidationError{
+					message: fmt.Sprintf("mandatory field \"%s\" is empty", key),
+				}
+			}
+		}
+
+		if field.Collection {
+			if !isEmpty(value) {
+				//TODO check and convert every array entry
+				if reflect.TypeOf(value).Kind() != reflect.Slice {
+					return nil, ErrValidationError{
+						message: fmt.Sprintf("collection field \"%s\" is not a collection", key),
+					}
+				}
+			}
+		}
+
+		if field.Type == model.FieldTypeBool {
+			switch v := value.(type) {
+			case float64:
+				data[key] = v > 0
+			case int:
+				data[key] = v > 0
+			case bool:
+				data[key] = v
+			}
+		}
+
+		if field.Type == model.FieldTypeTime {
+			switch v := value.(type) {
+			case string:
+				layout := "2006-01-02T15:04:05.000Z07:00"
+				time, err := time.Parse(layout, v)
+				if err != nil {
+					return nil, ErrValidationError{
+						message: fmt.Sprintf("wrong time format: key: \"%s\", err: %v", key, err),
+					}
+				}
+				data[key] = time
+			case float64:
+				data[key] = time.Unix(0, int64(v)*int64(time.Millisecond))
+			}
+		}
+	}
+
+	//check mandatory fields
+	for _, field := range modelDefinition.Fields {
+		if field.Mandatory {
+			if isEmpty(data[field.Name]) {
+				return nil, ErrValidationError{
+					message: fmt.Sprintf("mandatory field \"%s\" is empty", field.Name),
+				}
+			}
+		}
+	}
+	return data, nil
+}
+
+func isEmpty(value interface{}) bool {
+	if value == nil {
+		return true
+	}
+	switch v := value.(type) {
+	case string:
+		return v == ""
+	}
+	return false
+}
+
+//Store create a new model
+func Store(route model.Route, data model.JSONMap) (model.JSONMap, error) {
+	err := CheckRoute(route)
+	if err != nil {
+		return nil, err
+	}
+
+	// adding system attributes
+	data[internal.AttributeOwner] = route.Username
+	data[internal.AttributeCreated] = time.Now()
+	data[internal.AttributeModified] = time.Now()
+
+	modelid, err := dao.GetStorage().CreateModel(route, data)
+	if err != nil {
+		return nil, err
+	}
+	IncModelCounter(route.GetRouteName(), 1)
+	route.Identity = modelid
+	modelData, err := Get(route)
+	if err != nil {
+		return nil, err
+	}
+	return modelData, nil
+}
+
+//StoreMany create a bunch of new model
+func StoreMany(route model.Route, datas []model.JSONMap) ([]string, error) {
+	err := CheckRoute(route)
+	if err != nil {
+		return nil, err
+	}
+	now := time.Now()
+	for _, data := range datas {
+
+		// adding system attributes
+		data[internal.AttributeOwner] = route.Username
+		data[internal.AttributeCreated] = now
+		data[internal.AttributeModified] = now
+
+	}
+
+	modelids, err := dao.GetStorage().CreateModels(route, datas)
+	if err != nil {
+		return nil, err
+	}
+	return modelids, nil
+}
+
+//Get getting one model
+func Get(route model.Route) (model.JSONMap, error) {
+	err := CheckRoute(route)
+	if err != nil {
+		return nil, err
+	}
+
+	model, err := dao.GetStorage().GetModel(route)
+	if err != nil {
+		return nil, err
+	}
+	return model, nil
+}
+
+//Update update an existing model
+func Update(route model.Route, data model.JSONMap) (model.JSONMap, error) {
+	err := CheckRoute(route)
+	if err != nil {
+		return nil, err
+	}
+
+	if route.Identity == "" {
+		return nil, ErrMissingID
+	}
+
+	dataModel, err := dao.GetStorage().GetModel(route)
+	if err != nil {
+		return nil, err
+	}
+
+	// adding system attributes
+	data[internal.AttributeID] = route.Identity
+	data[internal.AttributeOwner] = route.Username
+	data[internal.AttributeCreated] = dataModel[internal.AttributeCreated]
+	data[internal.AttributeModified] = time.Now()
+
+	modelData, err := dao.GetStorage().UpdateModel(route, data)
+	if err != nil {
+		fmt.Printf("%v\n", err)
+		return nil, err
+	}
+	return modelData, nil
+}
+
+//Delete delete an existing model
+func Delete(route model.Route, deleteRef bool) error {
+	err := CheckRoute(route)
+	if err != nil {
+		return err
+	}
+
+	if route.Identity == "" {
+		return ErrMissingID
+	}
+	data, err := dao.GetStorage().GetModel(route)
+	if err != nil {
+		return err
+	}
+
+	beModel, ok := model.BackendList.Get(route.Backend)
+	if ok {
+		fmt.Printf("getting backend %s\n", beModel.Backendname)
+
+		if beModel.IsValidDatamodel(route.Model, data) && deleteRef {
+			files, err := beModel.GetReferencedFiles(route.Model, data)
+			if err != nil {
+				return err
+			}
+			for _, fileID := range files {
+				err = dao.GetStorage().DeleteFile(route.Backend, fileID)
+				if err != nil {
+					return err
+				}
+			}
+		}
+	}
+	err = dao.GetStorage().DeleteModel(route)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+//Query query for existing models
+func Query(route model.Route, query string, offset int, limit int) (int, []model.JSONMap, error) {
+	err := CheckRoute(route)
+	if err != nil {
+		return 0, nil, err
+	}
+
+	n, dataModels, err := dao.GetStorage().QueryModel(route, query, offset, limit)
+	if err != nil {
+		return 0, nil, err
+	}
+
+	return n, dataModels, nil
+}
+
+//GetCount query for existing models
+func GetCount(route model.Route) (int, error) {
+	err := CheckRoute(route)
+	if err != nil {
+		return 0, err
+	}
+
+	n, err := dao.GetStorage().CountModel(route)
+	if err != nil {
+		return 0, err
+	}
+
+	return n, nil
+}

+ 423 - 0
MessageService/worker/mqtt.go

@@ -0,0 +1,423 @@
+package worker
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	orglog "log"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	mqtt "github.com/eclipse/paho.mqtt.golang"
+	"github.com/willie68/AutoRestIoT/model"
+)
+
+const processorPrefix = "processor"
+const datasourcePrefix = "datasource"
+
+func CreateMQTTDestinationProcessor(backend string, destination model.Destination) (DestinationProcessor, error) {
+	processor := MQTTDestinationProcessor{
+		Backend:     backend,
+		Destination: destination,
+	}
+	return &processor, nil
+}
+
+//MQTTDestinationProcessor does nothing
+type MQTTDestinationProcessor struct {
+	Backend     string
+	Destination model.Destination
+}
+
+//Initialise the mqtt processor
+func (m *MQTTDestinationProcessor) Initialise(backend string, destination model.Destination) error {
+	// delete an already connected mqtt processor
+	//	if m.Destination != nil {
+	datasinkNsName := GetMQTTClientNsName(processorPrefix, backend, m.Destination.Name)
+	datasink, ok := mqttClients[datasinkNsName]
+	if ok {
+		if datasink.Client != nil {
+			datasink.Client.Disconnect(1000)
+		}
+	}
+	delete(mqttClients, datasinkNsName)
+	//	}
+	m.Destination = destination
+	// now initilaise the new connection
+	datasink, err := getDatasinkMQTTClient(datasinkNsName, backend, destination.Config.(model.DataSourceConfigMQTT))
+	if err != nil {
+		log.Alertf("%v", err)
+		return err
+	}
+	return nil
+}
+
+//Destroy the mqtt processor
+func (m *MQTTDestinationProcessor) Destroy(backend string, destination model.Destination) error {
+	datasinkNsName := GetMQTTClientNsName(processorPrefix, backend, m.Destination.Name)
+	datasink, ok := mqttClients[datasinkNsName]
+	if ok {
+		if datasink.Client != nil {
+			datasink.Client.Disconnect(1000)
+		}
+		delete(mqttClients, datasinkNsName)
+	}
+	return nil
+}
+
+//Store stores the message to a topic
+func (m *MQTTDestinationProcessor) Store(data model.JSONMap) (string, error) {
+	datasinkNsName := GetMQTTClientNsName(processorPrefix, m.Backend, m.Destination.Name)
+	datasink, ok := mqttClients[datasinkNsName]
+	if !ok {
+		err := m.Initialise(m.Backend, m.Destination)
+		if err != nil {
+			return "", err
+		}
+		datasinkNsName = GetMQTTClientNsName(processorPrefix, m.Backend, m.Destination.Name)
+		datasink, ok = mqttClients[datasinkNsName]
+		if !ok {
+			return "", errors.New("destination client is not ready")
+		}
+	}
+	payload, err := json.Marshal(data)
+	if err != nil {
+		return "", err
+	}
+	config := m.Destination.Config.(model.DataSourceConfigMQTT)
+	token := datasink.Client.Publish(datasink.Topic, byte(config.QoS), false, payload)
+	token.Wait()
+	if token.Error() != nil {
+		return "", token.Error()
+	}
+	return fmt.Sprintf("brk: %s, tpc: %s", datasink.Broker, datasink.Topic), nil
+}
+
+type MqttDatasource struct {
+	Client                   mqtt.Client
+	Broker                   string
+	Backend                  string
+	Destinations             []string
+	Topic                    string
+	QoS                      int
+	Payload                  string
+	TopicAttribute           string
+	SimpleValueAttribute     string
+	SimpleValueAttributeType string
+	Rule                     string
+}
+
+var mqttClients = make(map[string]MqttDatasource)
+
+//GetMQTTClients
+func GetMQTTClients() map[string]MqttDatasource {
+	return mqttClients
+}
+
+func init() {
+	//	mqtt.DEBUG = orglog.New(os.Stdout, "DEBUG", 0)
+	mqtt.ERROR = orglog.New(os.Stdout, "ERROR", 0)
+}
+
+func mqttStoreMessage(datasource MqttDatasource, msg mqtt.Message) {
+	//log.Infof("MODEL: %s.%s TOPIC: %s  MSG: %s", datasource.Backend, datasource.Model, msg.Topic(), msg.Payload())
+	data, err := prepareMessage(datasource, msg)
+	if err != nil {
+		log.Alertf("%v", err)
+		return
+	}
+
+	if datasource.TopicAttribute != "" {
+		data[datasource.TopicAttribute] = datasource.Topic
+	}
+
+	data, err = executeTransformationrule(datasource, data)
+	if err != nil {
+		log.Alertf("%v", err)
+		return
+	}
+
+	for _, destination := range datasource.Destinations {
+		if strings.HasPrefix(destination, "$model.") {
+			modelname := strings.TrimPrefix(destination, "$model.")
+			route := model.Route{
+				Backend: datasource.Backend,
+				Model:   modelname,
+			}
+			Store(route, data)
+		} else {
+			err := Destinations.Store(datasource.Backend, destination, data)
+			if err != nil {
+				log.Alertf("%v", err)
+				return
+			}
+		}
+	}
+}
+
+func executeTransformationrule(datasource MqttDatasource, data model.JSONMap) (model.JSONMap, error) {
+	if datasource.Rule != "" {
+		jsonBytes, err := json.Marshal(data)
+		if err != nil {
+			log.Alertf("%v", err)
+			return nil, err
+		}
+		newJson, err := Rules.TransformJSON(datasource.Backend, datasource.Rule, jsonBytes)
+		if err != nil {
+			log.Alertf("%v", err)
+			return nil, err
+		}
+
+		data = nil
+		err = json.Unmarshal(newJson, &data)
+		if err != nil {
+			log.Alertf("%v", err)
+			return nil, err
+		}
+		//fmt.Printf("src: %s\ndst: %s\n", string(jsonBytes), string(newJson))
+	}
+	return data, nil
+}
+
+func getSimpleDataAsModel(fieldname, fieldtype string, payload string) (model.JSONMap, error) {
+	data := model.JSONMap{}
+	var err error
+	switch fieldtype {
+	case model.FieldTypeInt:
+		value, err := strconv.Atoi(payload)
+		if err == nil {
+			data[fieldname] = value
+		}
+	case model.FieldTypeFloat:
+		value, err := strconv.ParseFloat(payload, 64)
+		if err == nil {
+			data[fieldname] = value
+		}
+	case model.FieldTypeTime:
+		value, err := time.Parse(time.RFC3339, payload)
+		if err != nil {
+			saveerr := err
+			var vint int
+			vint, err = strconv.Atoi(payload)
+			if err == nil {
+				value = time.Unix(0, int64(vint)*int64(time.Millisecond))
+			} else {
+				err = saveerr
+			}
+		}
+		if err == nil {
+			data[fieldname] = value
+		}
+	case model.FieldTypeBool:
+		value, err := strconv.ParseBool(payload)
+		if err == nil {
+			data[fieldname] = value
+		}
+	default:
+		data[fieldname] = payload
+	}
+	if err != nil {
+		return nil, err
+	}
+	return data, nil
+}
+
+func prepareMessage(datasource MqttDatasource, msg mqtt.Message) (model.JSONMap, error) {
+	var data model.JSONMap
+	data = nil
+	switch strings.ToLower(datasource.Payload) {
+	case "application/json":
+		err := json.Unmarshal(msg.Payload(), &data)
+		if err != nil {
+			log.Alertf("%v", err)
+			return nil, err
+		}
+	case "application/x.simple":
+		added := false
+		payload := string(msg.Payload())
+		var err error
+		data, err = getSimpleDataAsModel(datasource.SimpleValueAttribute, datasource.SimpleValueAttributeType, payload)
+		if err != nil {
+			log.Alertf("converting error on topic %s: %v", datasource.Topic, err)
+			return nil, err
+		}
+		if !added {
+			data[datasource.SimpleValueAttribute] = string(msg.Payload())
+		}
+	}
+	return data, nil
+}
+
+func mqttConnectionLost(datasource MqttDatasource, c mqtt.Client, e error) {
+	connected := false
+	for !connected {
+		err := mqttReconnect(c)
+		if err != nil {
+			log.Alertf("%v", err)
+			time.Sleep(10 * time.Second)
+			continue
+		}
+		connected = c.IsConnected()
+	}
+
+	subscribed := false
+	for !subscribed {
+		if !c.IsConnected() {
+			mqttReconnect(c)
+		}
+		err := mqttSubscribe(datasource)
+		if err != nil {
+			log.Alertf("%v", err)
+			time.Sleep(10 * time.Second)
+			continue
+		}
+		subscribed = true
+	}
+	log.Infof("registering topic %s on %s for model %v", datasource.Topic, datasource.Broker, datasource.Destinations)
+}
+
+func mqttReconnect(c mqtt.Client) error {
+	if !c.IsConnected() {
+		token := c.Connect()
+		token.Wait()
+		err := token.Error()
+		return err
+	}
+	return nil
+}
+
+func mqttSubscribe(datasource MqttDatasource) error {
+	token := datasource.Client.Subscribe(datasource.Topic, byte(datasource.QoS), func(c mqtt.Client, m mqtt.Message) {
+		mqttStoreMessage(datasource, m)
+	})
+	token.Wait()
+	err := token.Error()
+	return err
+}
+
+func mqttUnsubscribe(datasource MqttDatasource) error {
+	token := datasource.Client.Unsubscribe(datasource.Topic)
+	token.Wait()
+	err := token.Error()
+	return err
+}
+
+func getDatasinkMQTTClient(datasinkNsName string, backendname string, config model.DataSourceConfigMQTT) (MqttDatasource, error) {
+	datasourceMqtt := MqttDatasource{
+		Broker:                   config.Broker,
+		Backend:                  backendname,
+		Topic:                    config.Topic,
+		QoS:                      config.QoS,
+		Payload:                  config.Payload,
+		TopicAttribute:           config.AddTopicAsAttribute,
+		SimpleValueAttribute:     config.SimpleValueAttribute,
+		SimpleValueAttributeType: config.SimpleValueAttributeType,
+	}
+
+	opts := mqtt.NewClientOptions().AddBroker(config.Broker).SetClientID(datasinkNsName)
+	opts.SetKeepAlive(2 * time.Second)
+	//opts.SetDefaultPublishHandler(f)
+	opts.SetPingTimeout(1 * time.Second)
+	opts.AutoReconnect = true
+
+	opts.SetConnectionLostHandler(func(c mqtt.Client, err error) {
+		mqttConnectionLost(datasourceMqtt, c, err)
+	})
+	if config.Username != "" {
+		opts.CredentialsProvider = func() (string, string) {
+			return config.Username, config.Password
+		}
+	}
+
+	c := mqtt.NewClient(opts)
+	datasourceMqtt.Client = c
+
+	err := mqttReconnect(c)
+	if err != nil {
+		return MqttDatasource{}, err
+	}
+
+	mqttClients[datasinkNsName] = datasourceMqtt
+	return datasourceMqtt, nil
+}
+
+func getDatasourceMQTTClient(clientID string, backendname string, datasource model.DataSource) (MqttDatasource, error) {
+	destinationmodel := datasource.Destinations
+	config := datasource.Config.(model.DataSourceConfigMQTT)
+
+	datasourceMqtt := MqttDatasource{
+		Broker:                   config.Broker,
+		Backend:                  backendname,
+		Destinations:             destinationmodel,
+		Topic:                    config.Topic,
+		QoS:                      config.QoS,
+		Payload:                  config.Payload,
+		TopicAttribute:           config.AddTopicAsAttribute,
+		SimpleValueAttribute:     config.SimpleValueAttribute,
+		SimpleValueAttributeType: config.SimpleValueAttributeType,
+		Rule:                     datasource.Rule,
+	}
+
+	opts := mqtt.NewClientOptions().AddBroker(config.Broker).SetClientID(clientID)
+	opts.SetKeepAlive(2 * time.Second)
+	//opts.SetDefaultPublishHandler(f)
+	opts.SetPingTimeout(1 * time.Second)
+	opts.AutoReconnect = true
+
+	opts.SetConnectionLostHandler(func(c mqtt.Client, err error) {
+		mqttConnectionLost(datasourceMqtt, c, err)
+	})
+	if config.Username != "" {
+		opts.CredentialsProvider = func() (string, string) {
+			return config.Username, config.Password
+		}
+	}
+
+	c := mqtt.NewClient(opts)
+	datasourceMqtt.Client = c
+
+	err := mqttReconnect(c)
+	if err != nil {
+		return MqttDatasource{}, err
+	}
+
+	datasourceNSName := GetMQTTClientNsName(datasourcePrefix, datasourceMqtt.Backend, datasource.Name)
+	mqttClients[datasourceNSName] = datasourceMqtt
+	return datasourceMqtt, nil
+}
+
+func mqttRegisterTopic(clientID string, backendname string, datasource model.DataSource) error {
+	datasourceMqtt, err := getDatasourceMQTTClient(clientID, backendname, datasource)
+	if err != nil {
+		return err
+	}
+
+	err = mqttSubscribe(datasourceMqtt)
+	if err != nil {
+		return err
+	}
+
+	log.Infof("registering topic %s on %s for model %s", datasourceMqtt.Topic, datasourceMqtt.Broker, datasource.Destinations)
+	return nil
+}
+
+func mqttDeregisterTopic(clientID string, backendname string, datasource model.DataSource) error {
+	datasourceNSName := GetMQTTClientNsName(datasourcePrefix, backendname, datasource.Name)
+	datasourceMqtt, ok := mqttClients[datasourceNSName]
+	if ok {
+		err := mqttUnsubscribe(datasourceMqtt)
+		if err != nil {
+			return err
+		}
+		datasourceMqtt.Client.Disconnect(1000)
+		delete(mqttClients, datasourceNSName)
+		log.Infof("deregistering topic %s on %s for model %s", datasourceMqtt.Topic, datasourceMqtt.Broker, datasource.Destinations)
+	}
+	return nil
+}
+
+func GetMQTTClientNsName(prefix, backend, dataname string) string {
+	return fmt.Sprintf("%s.%s.%s", prefix, backend, dataname)
+}

+ 80 - 0
MessageService/worker/rules.go

@@ -0,0 +1,80 @@
+package worker
+
+import (
+	"errors"
+	"fmt"
+	"sync"
+
+	"github.com/qntfy/kazaam"
+)
+
+var ErrRuleNotDefined = errors.New("Rule not defined")
+
+type RuleList struct {
+	rules     map[string]kazaam.Kazaam
+	rulesSync sync.Mutex
+}
+
+var Rules = RuleList{
+	rules: make(map[string]kazaam.Kazaam),
+}
+
+var kazaamConfig kazaam.Config
+
+func init() {
+	kazaamConfig = kazaam.NewDefaultConfig()
+}
+
+//GetRuleName building the rule namespace name
+func GetRuleNsName(backendName string, rulename string) string {
+	return fmt.Sprintf("%s.%s", backendName, rulename)
+}
+
+//GetRulelist getting a copy of the list of rules
+func (r *RuleList) GetRulelist() []string {
+	list := make([]string, 0)
+	r.rulesSync.Lock()
+	for k, _ := range r.rules {
+		list = append(list, k)
+	}
+	r.rulesSync.Unlock()
+	return list
+}
+
+func (r *RuleList) Register(backendName, rulename string, config string) error {
+	name := GetRuleNsName(backendName, rulename)
+	k, err := kazaam.New(config, kazaamConfig)
+	if err != nil {
+		log.Alertf("Unable to transform message %v", err)
+		return err
+	}
+	r.rulesSync.Lock()
+	r.rules[name] = *k
+	r.rulesSync.Unlock()
+	return nil
+}
+
+func (r *RuleList) Deregister(backendName, rulename string) error {
+	name := GetRuleNsName(backendName, rulename)
+	r.rulesSync.Lock()
+	delete(r.rules, name)
+	r.rulesSync.Unlock()
+	return nil
+}
+
+func (r *RuleList) TransformJSON(backendName, rulename string, json []byte) ([]byte, error) {
+	name := GetRuleNsName(backendName, rulename)
+	r.rulesSync.Lock()
+	k, ok := r.rules[name]
+	r.rulesSync.Unlock()
+	if !ok {
+		return []byte{}, ErrRuleNotDefined
+	}
+	out, transformError := k.TransformInPlace(json)
+	if transformError != nil {
+		log.Alertf("Unable to transform message %v", transformError)
+		return []byte{}, transformError
+	}
+
+	return out, nil
+}

+ 185 - 0
MessageService/worker/rules_test.go

@@ -0,0 +1,185 @@
+package worker
+
+import (
+	"encoding/json"
+	"reflect"
+	"testing"
+)
+
+type TestRuleStruct struct {
+	RuleName string
+	RuleSrc  string
+	JsonSrc  string
+	JsonExp  string
+}
+
+var TestingRules = []TestRuleStruct{
+	TestRuleStruct{
+		RuleName: "tasmotaTemp",
+		RuleSrc: `[
+				{"operation": "shift",
+					"spec": {
+						"Temperature": "DS18B20.Temperature",
+						"TempUnit": "TempUnit",
+						"Time": "Time"
+					}
+				}
+			]`,
+		JsonSrc: `{"Time":"2020-04-27T08:47:07","DS18B20":{"Id":"0114556E95AA","Temperature":26.9},"TempUnit":"C"}`,
+		JsonExp: `{"Time":"2020-04-27T08:47:07", "Temperature":26.9,"TempUnit":"C"}`,
+	},
+	TestRuleStruct{
+		RuleName: "hmBWMMotion",
+		RuleSrc: `[
+				{"operation": "shift",
+					"spec": {
+						"Device": "hm.channelName",
+						"Motion": "hm.valueStable",
+						"Time": "ts"
+					}
+				}
+			]`,
+		JsonSrc: `{"val":false,"ts":1587971766000,"lc":1587971766000,"hm":{"ccu":"localhost","iface":"BidCos-RF","device":"LTK0028082","deviceName":"BWM Gartenhütte","deviceType":"HM-Sen-MDIR-O-2","channel":"LTK0028082:1","channelName":"BWM Gartenhütte","channelType":"MOTION_DETECTOR","channelIndex":1,"datapoint":"MOTION","datapointName":"BidCos-RF.LTK0028082:1.MOTION","datapointType":"BOOL","datapointMin":false,"datapointMax":true,"datapointDefault":false,"valueStable":false,"rooms":["Garten"],"room":"Garten","functions":["Homekit"],"function":"Homekit","ts":1587971766000,"lc":1587971766000,"change":false,"cache":true,"working":false,"uncertain":false,"stable":true}}`,
+		JsonExp: `{"Device":"BWM Gartenhütte", "Motion":false,"Time":1587971766000}`,
+	},
+	TestRuleStruct{
+		RuleName: "hmBWMBright",
+		RuleSrc: `[
+				{"operation": "shift",
+					"spec": {
+						"Device": "hm.channelName",
+						"Brightness": "hm.valueStable",
+						"Time": "ts"
+					}
+				}
+			]`,
+		JsonSrc: `{"val":181,"ts":1587973274220,"lc":1587973274220,"hm":{"ccu":"localhost","iface":"BidCos-RF","device":"LTK0028082","deviceName":"BWM Gartenhütte","deviceType":"HM-Sen-MDIR-O-2","channel":"LTK0028082:1","channelName":"BWM Gartenhütte","channelType":"MOTION_DETECTOR","channelIndex":1,"datapoint":"BRIGHTNESS","datapointName":"BidCos-RF.LTK0028082:1.BRIGHTNESS","datapointType":"INTEGER","datapointMin":0,"datapointMax":255,"datapointDefault":0,"valuePrevious":180,"valueStable":181,"rooms":["Garten"],"room":"Garten","functions":["Homekit"],"function":"Homekit","ts":1587973274220,"tsPrevious":1587972873227,"lc":1587973274220,"change":true,"cache":false,"uncertain":false,"stable":true}}`,
+		JsonExp: `{"Device":"BWM Gartenhütte", "Brightness":181,"Time":1587973274220}`,
+	},
+	TestRuleStruct{
+		RuleName: "hmTermTemp",
+		RuleSrc: `[
+				{"operation": "shift",
+					"spec": {
+						"Device": "hm.deviceName",
+						"Temperature": "hm.valueStable",
+						"Time": "ts"
+					}
+				}
+			]`,
+		JsonSrc: `{"val":22.6,"ts":1587973561640,"lc":1587973561640,"hm":{"ccu":"localhost","iface":"BidCos-RF","device":"OEQ1670535","deviceName":"Thermostat Bad","deviceType":"HM-TC-IT-WM-W-EU","channel":"OEQ1670535:1","channelName":"HM-TC-IT-WM-W-EU OEQ1670535:1","channelType":"WEATHER_TRANSMIT","channelIndex":1,"datapoint":"TEMPERATURE","datapointName":"BidCos-RF.OEQ1670535:1.TEMPERATURE","datapointType":"FLOAT","datapointMin":-10,"datapointMax":50,"datapointDefault":0,"datapointControl":"NONE","valuePrevious":22.7,"valueStable":22.6,"rooms":["Bad"],"room":"Bad","functions":["Heizung"],"function":"Heizung","ts":1587973561640,"tsPrevious":1587973426887,"lc":1587973561640,"change":true,"cache":false,"uncertain":false,"stable":true}}`,
+		JsonExp: `{"Device":"Thermostat Bad", "Temperature":22.6,"Time":1587973561640}`,
+	},
+	TestRuleStruct{
+		RuleName: "hmTermHumi",
+		RuleSrc: `[
+			{"operation": "shift",
+				"spec": {
+					"Device": "hm.deviceName",
+					"Humidity": "hm.valueStable",
+					"Time": "ts"
+				}
+			}, {
+             "operation": "timestamp",
+			 "spec": {
+				 "Time": {
+					"inputFormat": "$unixext",
+    				"outputFormat": "2006-01-02T15:04:05"
+				  }
+			   }
+			}
+		]`,
+		JsonSrc: `{"val":40, "ts":1587973561647,"lc":1587973561647,"hm":{"ccu":"localhost","iface":"BidCos-RF","device":"OEQ1670535","deviceName":"Thermostat Bad","deviceType":"HM-TC-IT-WM-W-EU","channel":"OEQ1670535:1","channelName":"HM-TC-IT-WM-W-EU OEQ1670535:1","channelType":"WEATHER_TRANSMIT","channelIndex":1,"datapoint":"HUMIDITY","datapointName":"BidCos-RF.OEQ1670535:1.HUMIDITY","datapointType":"INTEGER","datapointMin":0,"datapointMax":99,"datapointDefault":0,"datapointControl":"NONE","valuePrevious":39,"valueStable":40,"rooms":["Bad"],"room":"Bad","functions":["Heizung"],"function":"Heizung","ts":1587973561647,"tsPrevious":1587973426898,"lc":1587973561647,"change":true,"cache":false,"uncertain":false,"stable":true}}`,
+		JsonExp: `{"Device":"Thermostat Bad", "Humidity":40,"Time":"2020-04-27T09:46:01"}`,
+	},
+}
+
+func TestTasmotaRule(t *testing.T) {
+	for _, testRule := range TestingRules {
+
+		err := Rules.Register("mcs", testRule.RuleName, testRule.RuleSrc)
+		if err != nil {
+			t.Errorf("can't transforn: %v", err)
+			return
+		}
+
+		jsonDest, err := Rules.TransformJSON("mcs", testRule.RuleName, []byte(testRule.JsonSrc))
+		if err != nil {
+			t.Errorf("can't transforn: %v", err)
+			return
+		}
+
+		areEqual, _ := checkJSONBytesEqual(jsonDest, []byte(testRule.JsonExp))
+
+		if !areEqual {
+			t.Error("Transformed data does not match expectation.")
+			t.Log("Source:   ", testRule.JsonSrc)
+			t.Log("Expected:   ", testRule.JsonExp)
+			t.Log("Actual:     ", string(jsonDest))
+			t.FailNow()
+		}
+		t.Log("transformation OK.", string(jsonDest))
+	}
+}
+
+func TestSimpleRule(t *testing.T) {
+	jsonConfig := `[{
+  "operation": "shift",
+  "spec": {
+    "Temperature": "DS18B20.Temperature",
+    "TempUnit": "TempUnit"
+  }
+}]`
+
+	Rules.Register("mcs", "test.me", jsonConfig)
+
+	jsonObject := `{
+  "doc": {
+    "uid": 12345,
+    "guid": ["guid0", "guid2", "guid4"],
+    "guidObjects": [{"id": "guid0"}, {"id": "guid2"}, {"id": "guid4"}]
+  },
+  "top-level-key": null
+}`
+
+	jsonDest, err := Rules.TransformJSON("mcs", "test.me", []byte(jsonObject))
+	if err != nil {
+		t.Errorf("can't transforn: %v", err)
+		return
+	}
+	t.Log("transformation OK.", string(jsonDest))
+}
+
+func TestUnknownRule(t *testing.T) {
+	jsonObject := `{
+  "doc": {
+    "uid": 12345,
+    "guid": ["guid0", "guid2", "guid4"],
+    "guidObjects": [{"id": "guid0"}, {"id": "guid2"}, {"id": "guid4"}]
+  },
+  "top-level-key": null
+}`
+
+	_, err := Rules.TransformJSON("mcs", "test.me2", []byte(jsonObject))
+	if err != ErrRuleNotDefined {
+		t.Errorf("something goes wrong: %v", err)
+		return
+	}
+	t.Log("unknown rule throws error.")
+}
+
+func checkJSONBytesEqual(item1, item2 []byte) (bool, error) {
+	var out1, out2 interface{}
+
+	err := json.Unmarshal(item1, &out1)
+	if err != nil {
+		return false, nil
+	}
+
+	err = json.Unmarshal(item2, &out2)
+	if err != nil {
+		return false, nil
+	}
+
+	return reflect.DeepEqual(out1, out2), nil
+}

+ 7 - 0
msg-srv.code-workspace

@@ -0,0 +1,7 @@
+{
+	"folders": [
+		{
+			"path": ".\\MessageService"
+		}
+	]
+}