mirror of
https://github.com/mainflux/mainflux.git
synced 2025-04-24 13:48:49 +08:00
Use PostgreSQL as primary persistence solution (#175)
* Use normalizer as stream source Renamed 'writer' service to 'normalizer' and dropped Cassandra facilities from it. Extracted the common dependencies to 'mainflux' package for easier sharing. Fixed the API docs and unified environment variables. Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Use docker build arguments to specify build Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Remove cassandra libraries Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Update go-kit version to 0.6.0 Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Fix manager configuration Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Refactor docker-compose Merged individual compose files and dropped external links. Remove CoAP container since it is not referenced from NginX config at the moment. Update port mapping in compose and nginx.conf. Dropped bin scripts. Updated service documentation. Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Drop content-type check Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Implement users data access layer in PostgreSQL Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Bump version to 0.1.0 Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Use go-kit logger everywhere (except CoAP) Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Improve factory methods naming Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Implement clients data access layer on PostgreSQL Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Make tests stateless All tests are refactored to use map-based table-driven tests. No cross-tests dependencies is present anymore. Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Remove gitignore Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Fix nginx proxying Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Mark client-user FK explicit Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Update API documentation Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Update channel model Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Add channel PostgreSQL repository tests Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Implement PostgreSQL channels DAO Replaced update queries with raw SQL. Explicitly defined M2M table due to difficulties of ensuring the referential integrity through GORM. Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Expose connection endpoints Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Fix swagger docs and remove DB logging Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Fix nested query remarks Signed-off-by: Dejan Mijic <dejan@mainflux.com> * Add unique indices Signed-off-by: Dejan Mijic <dejan@mainflux.com>
This commit is contained in:
parent
b23ecb64e5
commit
ccd8965d6f
@ -1,8 +0,0 @@
|
||||
.git/
|
||||
.github/
|
||||
bin/
|
||||
glide.lock
|
||||
glide.yaml
|
||||
LICENSE
|
||||
MAINTAINERS
|
||||
README.md
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
||||
build
|
@ -1,9 +0,0 @@
|
||||
FROM golang:1.8-alpine AS builder
|
||||
WORKDIR /go/src/github.com/mainflux/mainflux
|
||||
COPY . .
|
||||
RUN cd cmd/coap && CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o exe
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /go/src/github.com/mainflux/mainflux/cmd/coap/exe /
|
||||
EXPOSE 5683
|
||||
ENTRYPOINT ["/exe"]
|
@ -1,9 +0,0 @@
|
||||
FROM golang:1.8-alpine AS builder
|
||||
WORKDIR /go/src/github.com/mainflux/mainflux
|
||||
COPY . .
|
||||
RUN cd cmd/http && CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o exe
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /go/src/github.com/mainflux/mainflux/cmd/http/exe /
|
||||
EXPOSE 7070
|
||||
ENTRYPOINT ["/exe"]
|
@ -1,9 +0,0 @@
|
||||
FROM golang:1.8-alpine AS builder
|
||||
WORKDIR /go/src/github.com/mainflux/mainflux
|
||||
COPY . .
|
||||
RUN cd cmd/manager && CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o exe
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /go/src/github.com/mainflux/mainflux/cmd/manager/exe /
|
||||
EXPOSE 8180
|
||||
ENTRYPOINT ["/exe"]
|
@ -1,8 +0,0 @@
|
||||
FROM golang:1.8-alpine AS builder
|
||||
WORKDIR /go/src/github.com/mainflux/mainflux
|
||||
COPY . .
|
||||
RUN cd cmd/writer && CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o exe
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /go/src/github.com/mainflux/mainflux/cmd/writer/exe /
|
||||
ENTRYPOINT ["/exe"]
|
236
Gopkg.lock
generated
236
Gopkg.lock
generated
@ -1,6 +1,27 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/Azure/go-ansiterm"
|
||||
packages = [
|
||||
".",
|
||||
"winterm"
|
||||
]
|
||||
revision = "d6e3b3328b783f23731bc4d058875b0371ff8109"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Microsoft/go-winio"
|
||||
packages = ["."]
|
||||
revision = "7da180ee92d8bd8bb8c37fc560e673e6557c392f"
|
||||
version = "v0.4.7"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/Nvveen/Gotty"
|
||||
packages = ["."]
|
||||
revision = "cd527374f1e5bff4938207604a14f2e38a9cf512"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/asaskevich/govalidator"
|
||||
packages = ["."]
|
||||
@ -14,14 +35,28 @@
|
||||
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/cenkalti/backoff"
|
||||
packages = ["."]
|
||||
revision = "61153c768f31ee5f130071d08fc82b85208528de"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/cisco/senml"
|
||||
packages = ["."]
|
||||
revision = "eb356817a51441202197049d8e7ae3c3755d8965"
|
||||
revision = "448c9510575e1dd6f780cb10addbad998cf80418"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/containerd/continuity"
|
||||
packages = ["pathdriver"]
|
||||
revision = "d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "6d212800a42e8ab5c146b8ace3490ee17e5225f9"
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@ -32,8 +67,52 @@
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "d2709f9f1f31ebcda9651b03077758c1f3a0018c"
|
||||
version = "v3.0.0"
|
||||
revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29"
|
||||
version = "v3.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/docker/docker"
|
||||
packages = [
|
||||
"api/types",
|
||||
"api/types/blkiodev",
|
||||
"api/types/container",
|
||||
"api/types/filters",
|
||||
"api/types/mount",
|
||||
"api/types/network",
|
||||
"api/types/registry",
|
||||
"api/types/strslice",
|
||||
"api/types/swarm",
|
||||
"api/types/swarm/runtime",
|
||||
"api/types/versions",
|
||||
"opts",
|
||||
"pkg/archive",
|
||||
"pkg/fileutils",
|
||||
"pkg/homedir",
|
||||
"pkg/idtools",
|
||||
"pkg/ioutils",
|
||||
"pkg/jsonmessage",
|
||||
"pkg/longpath",
|
||||
"pkg/mount",
|
||||
"pkg/pools",
|
||||
"pkg/stdcopy",
|
||||
"pkg/system",
|
||||
"pkg/term",
|
||||
"pkg/term/windows"
|
||||
]
|
||||
revision = "4d9beb4607404e4d756052aca7041517788f7e75"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-connections"
|
||||
packages = ["nat"]
|
||||
revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/docker/go-units"
|
||||
packages = ["."]
|
||||
revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52"
|
||||
version = "v0.3.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@ -41,6 +120,12 @@
|
||||
packages = ["."]
|
||||
revision = "ddcc80675fa42611359d91a6dfa5aa57fb90e72b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fsouza/go-dockerclient"
|
||||
packages = ["."]
|
||||
revision = "ca33ff277b527ce11b793e62f9ba244129b01caf"
|
||||
version = "1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-kit/kit"
|
||||
packages = [
|
||||
@ -51,8 +136,8 @@
|
||||
"metrics/prometheus",
|
||||
"transport/http"
|
||||
]
|
||||
revision = "fadad6fffe0466b19df9efd9acde5c9a52df5fa4"
|
||||
version = "v0.4.0"
|
||||
revision = "4dc7be5d2d12881735283bcab7352178e190fc71"
|
||||
version = "v0.6.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-logfmt/logfmt"
|
||||
@ -73,31 +158,31 @@
|
||||
version = "1.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gocql/gocql"
|
||||
packages = [
|
||||
".",
|
||||
"internal/lru",
|
||||
"internal/murmur",
|
||||
"internal/streams"
|
||||
]
|
||||
revision = "9ce8b08dfa5559eb86592129986654503cf40cf7"
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
|
||||
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/snappy"
|
||||
packages = ["."]
|
||||
revision = "553a641470496b2327abcac10b36396bd98e45c9"
|
||||
name = "github.com/jinzhu/gorm"
|
||||
packages = [
|
||||
".",
|
||||
"dialects/postgres"
|
||||
]
|
||||
revision = "58e34726dfc069b558038efbaa25555f182d1f7a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hailocab/go-hostpool"
|
||||
name = "github.com/jinzhu/inflection"
|
||||
packages = ["."]
|
||||
revision = "e80d13ce29ede4452c43dea11e79b9bc8a15b478"
|
||||
revision = "1c35d901db3da928c72a72d8458480cc9ade058f"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@ -107,9 +192,19 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
"hstore",
|
||||
"oid"
|
||||
]
|
||||
revision = "88edab0803230a3898347e77b474f8c1820a1f20"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
packages = ["pbutil"]
|
||||
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
|
||||
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/nats-io/go-nats"
|
||||
@ -118,19 +213,50 @@
|
||||
"encoders/builtin",
|
||||
"util"
|
||||
]
|
||||
revision = "29f9728a183bf3fa7e809e14edac00b33be72088"
|
||||
version = "v1.3.0"
|
||||
revision = "d66cb54e6b7bdd93f0b28afc8450d84c780dfb68"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/nats-io/nuid"
|
||||
packages = ["."]
|
||||
revision = "33c603157d6fd1b0ac2599bcc4a286b36479a06d"
|
||||
revision = "289cccf02c178dc782430d534e3c1f5b72af807f"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/opencontainers/go-digest"
|
||||
packages = ["."]
|
||||
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
|
||||
version = "v1.0.0-rc1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/opencontainers/image-spec"
|
||||
packages = [
|
||||
"specs-go",
|
||||
"specs-go/v1"
|
||||
]
|
||||
revision = "d60099175f88c47cd379c4738d158884749ed235"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/opencontainers/runc"
|
||||
packages = [
|
||||
"libcontainer/system",
|
||||
"libcontainer/user"
|
||||
]
|
||||
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "d8ed2627bdf02c080bf22230dbb337003b7aba2d"
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
@ -148,21 +274,37 @@
|
||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/common"
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model"
|
||||
]
|
||||
revision = "2e54d0b93cba2fd133edc32211dcc32c06ef72ca"
|
||||
revision = "89604d197083d4781071d3c65855d24ecfb0a563"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/util",
|
||||
"nfs",
|
||||
"xfs"
|
||||
]
|
||||
revision = "a6e9df898b1336106c743392c48ee0b71f5c4efa"
|
||||
revision = "282c8707aa210456a825798969cc27edda34992a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba"
|
||||
version = "v1.0.4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sony/gobreaker"
|
||||
@ -173,14 +315,14 @@
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
||||
version = "v1.1.4"
|
||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/ugorji/go"
|
||||
packages = ["codec"]
|
||||
revision = "f57d8945648dbfe4c332cff9c50fb57548958e3f"
|
||||
version = "v.1.1-beta"
|
||||
revision = "9831f2c3ac1068a78f50999a30db84270f647af6"
|
||||
version = "v1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/atomic"
|
||||
@ -204,34 +346,46 @@
|
||||
"internal/exit",
|
||||
"zapcore"
|
||||
]
|
||||
revision = "7a9ca91fa627ed52c7ff4fcc95cd044dc2c82a51"
|
||||
version = "v1.7.0"
|
||||
revision = "35aad584952c3e7020db7b839f6b102de6271f89"
|
||||
version = "v1.7.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"bcrypt",
|
||||
"blowfish"
|
||||
"blowfish",
|
||||
"ssh/terminal"
|
||||
]
|
||||
revision = "94eea52f7b742c7cbe0b03b22f0c4c8631ece122"
|
||||
revision = "650f4a345ab4e5b245a3034b110ebc7299e68186"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"context/ctxhttp"
|
||||
]
|
||||
revision = "d866cfc389cec985d6fda2859936a575a55a3ab6"
|
||||
revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/inf.v0"
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "88d2dcc510266da9f7f8c7f34e1940716cab5f5c"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
packages = ["."]
|
||||
revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4"
|
||||
version = "v0.9.0"
|
||||
revision = "15c8e8835bba04e0d7c2b57958ffe294d5e643dc"
|
||||
version = "v3.1.6"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "31542f4b815cef69fc1980da7cf626cf5e95b832660811039da81df223e43adc"
|
||||
inputs-digest = "9df3ffee2bc61a6ce786baa7570eb61d8275197ea36560cf0cfd7cc0ea53af96"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
36
Gopkg.toml
36
Gopkg.toml
@ -4,44 +4,60 @@
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dereulenspiegel/coap-mux"
|
||||
name = "github.com/cisco/senml"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
version = "3.0.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-coap"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-kit/kit"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/go-zoo/bone"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/gorm"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/nats-io/go-nats"
|
||||
version = "1.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/ory-am/dockertest.v3"
|
||||
version = "3.1.6"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sony/gobreaker"
|
||||
version = "0.3.0"
|
||||
name = "github.com/satori/go.uuid"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.1.4"
|
||||
name = "github.com/sony/gobreaker"
|
||||
version = "0.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "go.uber.org/zap"
|
||||
version = "1.7.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-coap"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dereulenspiegel/coap-mux"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
23
Makefile
23
Makefile
@ -1,23 +0,0 @@
|
||||
BUILD_DIR=build
|
||||
|
||||
all: manager http writer coap
|
||||
.PHONY: all manager http writer coap
|
||||
|
||||
manager:
|
||||
go build -o ${BUILD_DIR}/mainflux-manager cmd/manager/main.go
|
||||
|
||||
http:
|
||||
go build -o ${BUILD_DIR}/mainflux-http cmd/http/main.go
|
||||
|
||||
writer:
|
||||
go build -o ${BUILD_DIR}/mainflux-writer cmd/writer/main.go
|
||||
|
||||
coap:
|
||||
go build -o ${BUILD_DIR}/mainflux-coap cmd/coap/main.go
|
||||
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
|
||||
install:
|
||||
cp ${BUILD_DIR}/* $(GOBIN)
|
34
README.md
34
README.md
@ -19,40 +19,23 @@ For more details, check out the [official documentation][docs].
|
||||
|
||||
- Protocol bridging (i.e. HTTP, MQTT, WebSocket, CoAP)
|
||||
- Device management and provisioning
|
||||
- Linearly scalable [data storage][cassandra]
|
||||
- Fine-grained access control
|
||||
- Platform logging and instrumentation support
|
||||
- Container-based deployment using [Docker][docker]
|
||||
|
||||
## Quickstart
|
||||
|
||||
#### Docker
|
||||
Before proceeding, install the following prerequisites:
|
||||
|
||||
To start the docker composition, execute the [startup script](bin/mainflux-docker.sh) from `bin`
|
||||
directory:
|
||||
- [Docker](https://docs.docker.com/install/)
|
||||
- [Docker compose](https://docs.docker.com/compose/install/)
|
||||
|
||||
Once everything is installed, execute the following commands from project root:
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
docker-compose up -d
|
||||
```
|
||||
./mainflux-docker.sh start
|
||||
```
|
||||
|
||||
If the command successfully completes, you can verify that the all images are up & running by
|
||||
executing the following command in the terminal window:
|
||||
|
||||
```
|
||||
docker ps
|
||||
```
|
||||
|
||||
The composition can be terminated in the following way:
|
||||
|
||||
```
|
||||
./mainflux-docker.sh stop
|
||||
```
|
||||
|
||||
#### From sources
|
||||
|
||||
To download all the sources, and place them in appropriate locations (i.e. $GOPATH), use the
|
||||
[installation script](bin/mainflux-install.sh). Once it completes, the script will provide the
|
||||
instructions on how to finish the manual installation (i.e. install the required infrastructure).
|
||||
|
||||
## Contributing
|
||||
|
||||
@ -80,4 +63,3 @@ Thank you for your interest in Mainflux and wish to contribute!
|
||||
[grc-url]: https://goreportcard.com/report/github.com/mainflux/mainflux
|
||||
[license]: https://img.shields.io/badge/license-Apache%20v2.0-blue.svg
|
||||
[twitter]: https://twitter.com/mainflux
|
||||
[cassandra]: http://cassandra.apache.org
|
||||
|
@ -1,33 +0,0 @@
|
||||
###
|
||||
# Copyright (c) 2015-2017 Mainflux
|
||||
#
|
||||
# Mainflux server is licensed under an Apache license, version 2.0 license.
|
||||
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
|
||||
# See the included LICENSE file for more details.
|
||||
###
|
||||
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
|
||||
###
|
||||
# NATS
|
||||
###
|
||||
nats:
|
||||
image: nats:1.0.2
|
||||
container_name: mainflux-nats
|
||||
network_mode: bridge
|
||||
ports:
|
||||
- "4222:4222"
|
||||
- "8222:8222"
|
||||
|
||||
###
|
||||
# Cassandra
|
||||
###
|
||||
cassandra:
|
||||
image: cassandra:3.0.14
|
||||
container_name: mainflux-cassandra
|
||||
network_mode: bridge
|
||||
ports:
|
||||
- "9042:9042"
|
||||
- "9160:9160"
|
@ -1,92 +0,0 @@
|
||||
###
|
||||
# Copyright (c) 2015-2017 Mainflux
|
||||
#
|
||||
# Mainflux server is licensed under an Apache license, version 2.0 license.
|
||||
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
|
||||
# See the included LICENSE file for more details.
|
||||
###
|
||||
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
|
||||
###
|
||||
# Manager
|
||||
###
|
||||
manager:
|
||||
image: mainflux/manager:latest
|
||||
container_name: mainflux-manager
|
||||
network_mode: bridge
|
||||
ports:
|
||||
- "8180:8180"
|
||||
external_links:
|
||||
- mainflux-cassandra:cassandra
|
||||
environment:
|
||||
- MANAGER_DB_CLUSTER=cassandra
|
||||
- MANAGER_DB_KEYSPACE=manager
|
||||
|
||||
###
|
||||
# Message Writer
|
||||
###
|
||||
message-writer:
|
||||
image: mainflux/writer:latest
|
||||
container_name: mainflux-message-writer
|
||||
network_mode: bridge
|
||||
external_links:
|
||||
- mainflux-nats:nats
|
||||
- mainflux-cassandra:cassandra
|
||||
environment:
|
||||
- MESSAGE_WRITER_DB_CLUSTER=cassandra
|
||||
- MESSAGE_WRITER_DB_KEYSPACE=message_writer
|
||||
- MESSAGE_WRITER_NATS_URL=nats://nats:4222
|
||||
|
||||
###
|
||||
# MQTT Broker
|
||||
###
|
||||
mqtt-adapter:
|
||||
image: mainflux/mqtt-adapter:latest
|
||||
container_name: mainflux-mqtt
|
||||
ports:
|
||||
- "1883:1883"
|
||||
network_mode: bridge
|
||||
depends_on:
|
||||
- manager
|
||||
external_links:
|
||||
- mainflux-nats:nats
|
||||
- mainflux-manager:manager
|
||||
environment:
|
||||
- MQTT_ADAPTER_NATS_URL=nats://nats:4222
|
||||
- AUTH_URL=http://manager
|
||||
- AUTH_PORT=8180
|
||||
|
||||
###
|
||||
# HTTP Server
|
||||
###
|
||||
http-adapter:
|
||||
image: mainflux/http:latest
|
||||
container_name: mainflux-http
|
||||
network_mode: bridge
|
||||
depends_on:
|
||||
- manager
|
||||
ports:
|
||||
- "7070:7070"
|
||||
external_links:
|
||||
- mainflux-nats:nats
|
||||
- mainflux-manager:manager
|
||||
environment:
|
||||
- HTTP_ADAPTER_NATS_URL=nats://nats:4222
|
||||
- HTTP_ADAPTER_MANAGER_URL=http://manager:8180
|
||||
|
||||
###
|
||||
# CoAP Server
|
||||
###
|
||||
coap-adapter:
|
||||
image: mainflux/coap:latest
|
||||
container_name: mainflux-coap
|
||||
network_mode: bridge
|
||||
ports:
|
||||
- "5683:5683"
|
||||
external_links:
|
||||
- mainflux-nats:nats
|
||||
environment:
|
||||
- COAP_ADAPTER_NATS_URL=nats://nats:4222
|
@ -1,32 +0,0 @@
|
||||
###
|
||||
# Copyright (c) 2015-2017 Mainflux
|
||||
#
|
||||
# Mainflux server is licensed under an Apache license, version 2.0 license.
|
||||
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
|
||||
# See the included LICENSE file for more details.
|
||||
###
|
||||
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
|
||||
###
|
||||
# NGINX
|
||||
###
|
||||
nginx:
|
||||
image: nginx:1.13-alpine
|
||||
container_name: mainflux-nginx
|
||||
volumes:
|
||||
- $PWD/docker/nginx.conf:/etc/nginx/nginx.conf
|
||||
- $PWD/docker/ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
|
||||
- $PWD/docker/ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
|
||||
- $PWD/docker/ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
|
||||
network_mode: bridge
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8883:8883"
|
||||
external_links:
|
||||
- mainflux-manager
|
||||
- mainflux-http
|
||||
- mainflux-mqtt
|
@ -1,219 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Derived from https://github.com/alphabetum/bash-boilerplate
|
||||
|
||||
# Strict Mode
|
||||
set -o nounset
|
||||
|
||||
# Exit immediately if a pipeline returns non-zero.
|
||||
#set -o errexit
|
||||
|
||||
# Print a helpful message if a pipeline with non-zero exit code causes the
|
||||
# script to exit as described above.
|
||||
trap 'echo "Aborting due to errexit on line $LINENO. Exit code: $?" >&2' ERR
|
||||
|
||||
# Allow the above trap be inherited by all functions in the script.
|
||||
# Short form: set -E
|
||||
set -o errtrace
|
||||
|
||||
# Return value of a pipeline is the value of the last (rightmost) command to
|
||||
# exit with a non-zero status, or zero if all commands in the pipeline exit
|
||||
# successfully.
|
||||
set -o pipefail
|
||||
|
||||
# Set IFS to just newline and tab at the start
|
||||
DEFAULT_IFS="${IFS}"
|
||||
SAFER_IFS=$'\n\t'
|
||||
IFS="${SAFER_IFS}"
|
||||
|
||||
###############################################################################
|
||||
# Environment
|
||||
###############################################################################
|
||||
|
||||
# $_ME
|
||||
#
|
||||
# Set to the program's basename.
|
||||
_ME=$(basename "${0}")
|
||||
|
||||
###############################################################################
|
||||
# Help
|
||||
###############################################################################
|
||||
|
||||
# _print_help()
|
||||
#
|
||||
# Usage:
|
||||
# _print_help
|
||||
#
|
||||
# Print the program help information.
|
||||
_print_help() {
|
||||
cat <<HEREDOC
|
||||
MAINFLUX-DOCKER
|
||||
|
||||
Starts or stops Mainflux Docker composition.
|
||||
|
||||
Commands:
|
||||
start Start Docker composition
|
||||
stop Stop Docker composition
|
||||
|
||||
Options:
|
||||
-h, --help Show this screen.
|
||||
HEREDOC
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Program Functions
|
||||
###############################################################################
|
||||
|
||||
_start() {
|
||||
|
||||
# Start NATS, Cassandra and Nginx
|
||||
printf "Starting NATS and Cassandra...\n\n"
|
||||
|
||||
NB_DOCKERS=$(docker ps -a -f name=mainflux-nats -f name=mainflux-cassandra | wc -l)
|
||||
if [[ $NB_DOCKERS -lt 3 ]]
|
||||
then
|
||||
docker-compose -f docker/docker-compose.infrastructure.yml pull
|
||||
docker-compose -f docker/docker-compose.infrastructure.yml create
|
||||
fi
|
||||
docker-compose -f docker/docker-compose.infrastructure.yml start
|
||||
|
||||
# Check if C* is alive
|
||||
printf "\nWaiting for Cassandra to start. This takes time, please be patient...\n"
|
||||
|
||||
# Wait until Cassandra is ready to accept cqlsh commands
|
||||
# or timeout after 15 sec
|
||||
c_on=0
|
||||
retries=0
|
||||
while [[ $retries -lt 15 ]]
|
||||
do
|
||||
# Check if C* port 9042 is open
|
||||
if $(docker exec -it mainflux-cassandra cqlsh -e "exit" > /dev/null 2>&1)
|
||||
then
|
||||
# Sucess
|
||||
c_on=1
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
printf "."
|
||||
retries=$((retries + 1))
|
||||
done
|
||||
|
||||
# If Cassandra did not start then shut down everything and exit
|
||||
if [[ $c_on -eq 0 ]]
|
||||
then
|
||||
printf "\nCassandra did not start - shuting down everything.\n"
|
||||
docker-compose -f docker/docker-compose.infrastructure.yml stop
|
||||
exit 0
|
||||
else
|
||||
printf "OK\n"
|
||||
fi
|
||||
|
||||
# Create C* keyspaces, if missing
|
||||
docker exec -it mainflux-cassandra cqlsh -e "CREATE KEYSPACE IF NOT EXISTS manager WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"
|
||||
docker exec -it mainflux-cassandra cqlsh -e "CREATE KEYSPACE IF NOT EXISTS message_writer WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"
|
||||
|
||||
# Start Mainflux
|
||||
printf "\nStarting Mainflux composition...\n\n"
|
||||
|
||||
NB_DOCKERS=$(docker ps -a -f name=mainflux-manager -f name=mainflux-http -f name=mainflux-mqtt -f name=mainflux-coap -f name=mainflux-message-writer | wc -l)
|
||||
if [[ $NB_DOCKERS -lt 6 ]]
|
||||
then
|
||||
docker-compose -f docker/docker-compose.mainflux.yml pull
|
||||
docker-compose -f docker/docker-compose.mainflux.yml create
|
||||
fi
|
||||
docker-compose -f docker/docker-compose.mainflux.yml start
|
||||
|
||||
# Start Nginx
|
||||
printf "\nStarting Nginx...\n\n"
|
||||
|
||||
NB_DOCKERS=$(docker ps -a -f name=mainflux-nginx | wc -l)
|
||||
if [[ $NB_DOCKERS -lt 2 ]]
|
||||
then
|
||||
docker-compose -f docker/docker-compose.nginx.yml pull
|
||||
docker-compose -f docker/docker-compose.nginx.yml create
|
||||
fi
|
||||
docker-compose -f docker/docker-compose.nginx.yml start
|
||||
|
||||
if [[ $? -ne 0 ]]
|
||||
then
|
||||
_stop
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "\n*** MAINFLUX IS ON ***\n\n"
|
||||
|
||||
docker ps
|
||||
}
|
||||
|
||||
_stop() {
|
||||
printf "\nStopping Nginx...\n\n"
|
||||
docker-compose -f docker/docker-compose.nginx.yml stop
|
||||
|
||||
printf "\nStopping Mainflux composition...\n\n"
|
||||
docker-compose -f docker/docker-compose.mainflux.yml stop
|
||||
|
||||
printf "\nStopping NATS and Cassandra...\n\n"
|
||||
docker-compose -f docker/docker-compose.infrastructure.yml stop
|
||||
|
||||
printf "\n*** MAINFLUX IS OFF ***\n\n"
|
||||
}
|
||||
|
||||
_clean() {
|
||||
printf "\nCleaning NATS and Cassandra containers...\n\n"
|
||||
docker-compose -f docker/docker-compose.infrastructure.yml rm -f
|
||||
|
||||
printf "\nCleaning Mainflux containers...\n\n"
|
||||
docker-compose -f docker/docker-compose.mainflux.yml rm -f
|
||||
|
||||
printf "\nCleaning Nginx container...\n\n"
|
||||
docker-compose -f docker/docker-compose.nginx.yml rm -f
|
||||
|
||||
printf "\n*** Docker containers cleaned ***\n\n"
|
||||
}
|
||||
|
||||
_mainflux_docker() {
|
||||
if [[ $1 == "start" ]]
|
||||
then
|
||||
_start
|
||||
elif [[ $1 == "stop" ]]
|
||||
then
|
||||
_stop
|
||||
elif [[ $1 == "clean" ]]
|
||||
then
|
||||
_clean
|
||||
else
|
||||
printf "Unknown command.\n"
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Main
|
||||
###############################################################################
|
||||
|
||||
# _main()
|
||||
#
|
||||
# Usage:
|
||||
# _main [<options>] [<arguments>]
|
||||
#
|
||||
# Description:
|
||||
# Entry point for the program, handling basic option parsing and dispatching.
|
||||
_main() {
|
||||
|
||||
# No arguments provided
|
||||
if [[ $# -eq 0 ]] ; then
|
||||
_print_help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Avoid complex option parsing when only one program option is expected.
|
||||
if [[ "${1:-}" =~ ^-h|--help$ ]]
|
||||
then
|
||||
_print_help
|
||||
else
|
||||
_mainflux_docker "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Call `_main` after everything has been defined.
|
||||
_main "$@"
|
@ -1,85 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
DIR=$PWD
|
||||
|
||||
mkdir -p ./mainflux
|
||||
cd ./mainflux
|
||||
|
||||
if [ -z "$GOPATH" ]; then
|
||||
mkdir -p $PWD/go
|
||||
export GOPATH=$PWD/go
|
||||
fi
|
||||
|
||||
export GOBIN=$GOPATH/bin
|
||||
export PATH=$PATH:$GOBIN
|
||||
mkdir -p $GOBIN
|
||||
|
||||
# Mainflux Go microservices
|
||||
go get -d -v github.com/mainflux/mainflux
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux
|
||||
make
|
||||
make install
|
||||
cd -
|
||||
|
||||
|
||||
# MQTT
|
||||
git clone https://github.com/mainflux/mqtt-adapter
|
||||
cd mqtt-adapter
|
||||
npm install
|
||||
cd ..
|
||||
|
||||
# NGINX Conf
|
||||
git clone https://github.com/mainflux/proxy
|
||||
|
||||
# NATS
|
||||
go get -v github.com/nats-io/gnatsd
|
||||
|
||||
# Make symlink to go mainflux sources
|
||||
ln -s $GOPATH/src/github.com/mainflux mainflux-go
|
||||
|
||||
# Go back to where we started
|
||||
cd $DIR
|
||||
|
||||
# Print info
|
||||
cat << EOF
|
||||
|
||||
***
|
||||
|
||||
# Mainflux is now installed #
|
||||
|
||||
- Go sources are located at $GOPATH/src
|
||||
- Go binaries are located are $GOBIN
|
||||
- MQTT NodeJS sources are located at $PWD/mainflux/mqtt-adapter
|
||||
- NGINX config files are located in $PWD/mainflux/nginx-conf
|
||||
|
||||
External dependencies needed for Mainflux are:
|
||||
- Cassandra
|
||||
- NATS
|
||||
- NGINX
|
||||
|
||||
NATS has been installed.
|
||||
For Cassandra follow the instructions at http://cassandra.apache.org/download/
|
||||
|
||||
After installing Cassandra you should create the two keyspaces that Mainflux uses. This can be done with something similar to:
|
||||
|
||||
cqlsh> CREATE KEYSPACE IF NOT EXISTS manager WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };
|
||||
cqlsh> CREATE KEYSPACE IF NOT EXISTS message_writer WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };
|
||||
|
||||
Please note that in the example SQL statment above, the keyspaces will be created in a single datacenter (single cluster) and there will
|
||||
only be one replica (copy) of the data. You should create the keyspaces with parameters appropriate for your Cassandra installation. Take a look
|
||||
at the Cassandra documentation for creating keyspaces for more details. For production usage you should always configure multiple replicas in order
|
||||
to have data redundancy and be safe in case one or more Cassandra cluster nodes fail.
|
||||
|
||||
|
||||
For NGINX follow the instructions here: http://nginx.org/en/docs/install.html
|
||||
|
||||
NGINX config has been cloned in nginx-conf,
|
||||
and these config files have to be copied to /etc/nginx once NGINX server
|
||||
is installed on the system.
|
||||
After copying these files you have to re-start the nginx service:
|
||||
|
||||
sudo systemctl restart nginx.service
|
||||
|
||||
***
|
||||
|
||||
EOF
|
@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
###
|
||||
# Launches all Mainflux Go binaries when they are installed globally.
|
||||
# Also launches NATS broker instance, expecting that
|
||||
# `gnatsd` is installed globally.
|
||||
#
|
||||
# Expects that Cassandra is already installed and running.
|
||||
#
|
||||
# Does not launch NodeJS MQTT service - this one must be launched by hand for now.
|
||||
###
|
||||
|
||||
# Kill all mainflux-* stuff
|
||||
function cleanup {
|
||||
pkill mainflux
|
||||
}
|
||||
|
||||
gnatsd &
|
||||
# Wait a bit for NATS to be on
|
||||
sleep 0.1
|
||||
mainflux-http &
|
||||
mainflux-manager &
|
||||
mainflux-writer &
|
||||
mainflux-coap &
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
while : ; do sleep 1 ; done
|
||||
|
||||
|
@ -33,11 +33,11 @@ func main() {
|
||||
nc := connectToNats(cfg, logger)
|
||||
defer nc.Close()
|
||||
|
||||
repo := nats.NewMessageRepository(nc)
|
||||
ca := adapter.NewCoAPAdapter(logger, repo)
|
||||
pub := nats.NewMessagePublisher(nc)
|
||||
ca := adapter.NewCoAPAdapter(logger, pub)
|
||||
|
||||
nc.Subscribe("msg.http", ca.BridgeHandler)
|
||||
nc.Subscribe("msg.mqtt", ca.BridgeHandler)
|
||||
nc.Subscribe("src.http", ca.BridgeHandler)
|
||||
nc.Subscribe("src.mqtt", ca.BridgeHandler)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
adapter "github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/http/api"
|
||||
"github.com/mainflux/mainflux/http/nats"
|
||||
@ -18,24 +19,25 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
port int = 7070
|
||||
defPort string = "8180"
|
||||
defNatsURL string = broker.DefaultURL
|
||||
envNatsURL string = "HTTP_ADAPTER_NATS_URL"
|
||||
defManagerURL string = "http://localhost:8180"
|
||||
envManagerURL string = "HTTP_ADAPTER_MANAGER_URL"
|
||||
envPort string = "MF_HTTP_ADAPTER_PORT"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envManagerURL string = "MF_MANAGER_URL"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Port int
|
||||
NatsURL string
|
||||
ManagerURL string
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
Port: port,
|
||||
NatsURL: getenv(envNatsURL, defNatsURL),
|
||||
ManagerURL: getenv(envManagerURL, defManagerURL),
|
||||
ManagerURL: mainflux.Env(envManagerURL, defManagerURL),
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
|
||||
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
|
||||
@ -43,37 +45,35 @@ func main() {
|
||||
|
||||
nc, err := broker.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Log("aborted", err)
|
||||
logger.Log("error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
repo := nats.NewMessageRepository(nc)
|
||||
svc := adapter.NewService(repo)
|
||||
pub := nats.NewMessagePublisher(nc)
|
||||
|
||||
svc = api.NewLoggingService(logger, svc)
|
||||
|
||||
fields := []string{"method"}
|
||||
svc = api.NewMetricService(
|
||||
svc := adapter.New(pub)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "http_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, fields),
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "http_adapter",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, fields),
|
||||
svc,
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%d", cfg.Port)
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
mc := manager.NewClient(cfg.ManagerURL)
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc, mc))
|
||||
}()
|
||||
@ -86,12 +86,3 @@ func main() {
|
||||
|
||||
logger.Log("terminated", <-errs)
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
@ -5,101 +5,95 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
kitprometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/api"
|
||||
"github.com/mainflux/mainflux/manager/bcrypt"
|
||||
"github.com/mainflux/mainflux/manager/cassandra"
|
||||
"github.com/mainflux/mainflux/manager/jwt"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
port int = 8180
|
||||
sep string = ","
|
||||
defCluster string = "127.0.0.1"
|
||||
defKeyspace string = "manager"
|
||||
defSecret string = "manager"
|
||||
envCluster string = "MANAGER_DB_CLUSTER"
|
||||
envKeyspace string = "MANAGER_DB_KEYSPACE"
|
||||
envSecret string = "MANAGER_SECRET"
|
||||
defDBHost string = "localhost"
|
||||
defDBPort string = "5432"
|
||||
defDBUser string = "mainflux"
|
||||
defDBPass string = "mainflux"
|
||||
defDBName string = "manager"
|
||||
defPort string = "8180"
|
||||
defSecret string = "manager"
|
||||
envDBHost string = "MF_DB_HOST"
|
||||
envDBPort string = "MF_DB_PORT"
|
||||
envDBUser string = "MF_DB_USER"
|
||||
envDBPass string = "MF_DB_PASS"
|
||||
envDBName string = "MF_MANAGER_DB"
|
||||
envPort string = "MF_MANAGER_PORT"
|
||||
envSecret string = "MF_MANAGER_SECRET"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Port int
|
||||
Cluster string
|
||||
Keyspace string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return value
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPass string
|
||||
DBName string
|
||||
Port string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
Port: port,
|
||||
Cluster: getenv(envCluster, defCluster),
|
||||
Keyspace: getenv(envKeyspace, defKeyspace),
|
||||
Secret: getenv(envSecret, defSecret),
|
||||
DBHost: mainflux.Env(envDBHost, defDBHost),
|
||||
DBPort: mainflux.Env(envDBPort, defDBPort),
|
||||
DBUser: mainflux.Env(envDBUser, defDBUser),
|
||||
DBPass: mainflux.Env(envDBPass, defDBPass),
|
||||
DBName: mainflux.Env(envDBName, defDBName),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
Secret: mainflux.Env(envSecret, defSecret),
|
||||
}
|
||||
|
||||
var logger log.Logger
|
||||
logger = log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
|
||||
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
|
||||
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
|
||||
|
||||
session, err := cassandra.Connect(strings.Split(cfg.Cluster, sep), cfg.Keyspace)
|
||||
db, err := postgres.Connect(cfg.DBHost, cfg.DBPort, cfg.DBName, cfg.DBUser, cfg.DBPass)
|
||||
if err != nil {
|
||||
logger.Log("error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer session.Close()
|
||||
defer db.Close()
|
||||
|
||||
if err := cassandra.Initialize(session); err != nil {
|
||||
logger.Log("error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
users := postgres.NewUserRepository(db)
|
||||
clients := postgres.NewClientRepository(db)
|
||||
channels := postgres.NewChannelRepository(db)
|
||||
hasher := bcrypt.New()
|
||||
idp := jwt.New(cfg.Secret)
|
||||
|
||||
users := cassandra.NewUserRepository(session)
|
||||
clients := cassandra.NewClientRepository(session)
|
||||
channels := cassandra.NewChannelRepository(session)
|
||||
hasher := bcrypt.NewHasher()
|
||||
idp := jwt.NewIdentityProvider(cfg.Secret)
|
||||
|
||||
var svc manager.Service
|
||||
svc = manager.NewService(users, clients, channels, hasher, idp)
|
||||
svc = api.NewLoggingService(logger, svc)
|
||||
|
||||
fields := []string{"method"}
|
||||
svc = api.NewMetricService(
|
||||
svc := manager.New(users, clients, channels, hasher, idp)
|
||||
svc = api.LoggingMiddleware(svc, logger)
|
||||
svc = api.MetricsMiddleware(
|
||||
svc,
|
||||
kitprometheus.NewCounterFrom(stdprometheus.CounterOpts{
|
||||
Namespace: "manager",
|
||||
Subsystem: "api",
|
||||
Name: "request_count",
|
||||
Help: "Number of requests received.",
|
||||
}, fields),
|
||||
}, []string{"method"}),
|
||||
kitprometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
|
||||
Namespace: "manager",
|
||||
Subsystem: "api",
|
||||
Name: "request_latency_microseconds",
|
||||
Help: "Total duration of requests in microseconds.",
|
||||
}, fields),
|
||||
svc,
|
||||
}, []string{"method"}),
|
||||
)
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%d", cfg.Port)
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
errs <- http.ListenAndServe(p, api.MakeHandler(svc))
|
||||
}()
|
||||
|
||||
|
59
cmd/normalizer/main.go
Normal file
59
cmd/normalizer/main.go
Normal file
@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/mainflux/mainflux/normalizer"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
defNatsURL string = nats.DefaultURL
|
||||
defPort string = "8180"
|
||||
envNatsURL string = "MF_NATS_URL"
|
||||
envPort string = "MF_NORMALIZER_PORT"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
NatsURL string
|
||||
Port string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := config{
|
||||
NatsURL: mainflux.Env(envNatsURL, defNatsURL),
|
||||
Port: mainflux.Env(envPort, defPort),
|
||||
}
|
||||
|
||||
logger := log.NewJSONLogger(log.NewSyncWriter(os.Stdout))
|
||||
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
|
||||
|
||||
nc, err := nats.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Log("error", fmt.Sprintf("Failed to connect: %s", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
errs := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
p := fmt.Sprintf(":%s", cfg.Port)
|
||||
errs <- http.ListenAndServe(p, normalizer.MakeHandler())
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
normalizer.Subscribe(nc, logger)
|
||||
logger.Log("terminated", <-errs)
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mainflux/mainflux/writer"
|
||||
"github.com/mainflux/mainflux/writer/cassandra"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
sep string = ","
|
||||
subject string = "msg.*"
|
||||
queue string = "message_writers"
|
||||
defCluster string = "127.0.0.1"
|
||||
defKeyspace string = "message_writer"
|
||||
defNatsURL string = nats.DefaultURL
|
||||
envCluster string = "MESSAGE_WRITER_DB_CLUSTER"
|
||||
envKeyspace string = "MESSAGE_WRITER_DB_KEYSPACE"
|
||||
envNatsURL string = "MESSAGE_WRITER_NATS_URL"
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
|
||||
type config struct {
|
||||
Cluster string
|
||||
Keyspace string
|
||||
NatsURL string
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := loadConfig()
|
||||
|
||||
logger, _ = zap.NewProduction()
|
||||
defer logger.Sync()
|
||||
|
||||
session := connectToCassandra(cfg)
|
||||
defer session.Close()
|
||||
|
||||
nc := connectToNats(cfg)
|
||||
defer nc.Close()
|
||||
|
||||
repo := makeRepository(session)
|
||||
|
||||
nc.QueueSubscribe(subject, queue, func(m *nats.Msg) {
|
||||
msg := writer.RawMessage{}
|
||||
|
||||
if err := json.Unmarshal(m.Data, &msg); err != nil {
|
||||
logger.Error("Failed to unmarshal raw message.", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo.Save(msg); err != nil {
|
||||
logger.Error("Failed to save message.", zap.Error(err))
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
forever()
|
||||
}
|
||||
|
||||
func loadConfig() *config {
|
||||
return &config{
|
||||
Cluster: env(envCluster, defCluster),
|
||||
Keyspace: env(envKeyspace, defKeyspace),
|
||||
NatsURL: env(envNatsURL, defNatsURL),
|
||||
}
|
||||
}
|
||||
|
||||
func env(key, fallback string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func connectToCassandra(cfg *config) *gocql.Session {
|
||||
hosts := strings.Split(cfg.Cluster, sep)
|
||||
|
||||
s, err := cassandra.Connect(hosts, cfg.Keyspace)
|
||||
if err != nil {
|
||||
logger.Error("Failed to connect to DB", zap.Error(err))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func makeRepository(session *gocql.Session) writer.MessageRepository {
|
||||
if err := cassandra.Initialize(session); err != nil {
|
||||
logger.Error("Failed to initialize message repository.", zap.Error(err))
|
||||
}
|
||||
|
||||
return cassandra.NewMessageRepository(session)
|
||||
}
|
||||
|
||||
func connectToNats(cfg *config) *nats.Conn {
|
||||
nc, err := nats.Connect(cfg.NatsURL)
|
||||
if err != nil {
|
||||
logger.Error("Failed to connect to NATS.", zap.Error(err))
|
||||
}
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
func forever() {
|
||||
errs := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, syscall.SIGINT)
|
||||
errs <- fmt.Errorf("%s", <-c)
|
||||
}()
|
||||
|
||||
<-errs
|
||||
}
|
@ -6,7 +6,7 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/dustin/go-coap"
|
||||
"github.com/mainflux/mainflux/writer"
|
||||
"github.com/mainflux/mainflux"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@ -22,18 +22,17 @@ type Observer struct {
|
||||
type CoAPAdapter struct {
|
||||
obsMap map[string][]Observer
|
||||
logger *zap.Logger
|
||||
repo writer.MessageRepository
|
||||
pub mainflux.MessagePublisher
|
||||
}
|
||||
|
||||
// NewCoAPAdapter creates new CoAP adapter struct
|
||||
func NewCoAPAdapter(logger *zap.Logger, repo writer.MessageRepository) *CoAPAdapter {
|
||||
func NewCoAPAdapter(logger *zap.Logger, pub mainflux.MessagePublisher) *CoAPAdapter {
|
||||
ca := &CoAPAdapter{
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
pub: pub,
|
||||
obsMap: make(map[string][]Observer),
|
||||
}
|
||||
|
||||
ca.obsMap = make(map[string][]Observer)
|
||||
|
||||
return ca
|
||||
}
|
||||
|
||||
@ -48,7 +47,7 @@ func (ca *CoAPAdapter) BridgeHandler(nm *broker.Msg) {
|
||||
log.Printf("Received a message: %s\n", string(nm.Data))
|
||||
|
||||
// And write it into the database
|
||||
m := writer.RawMessage{}
|
||||
m := mainflux.RawMessage{}
|
||||
if len(nm.Data) > 0 {
|
||||
if err := json.Unmarshal(nm.Data, &m); err != nil {
|
||||
log.Println("Can not decode adapter msg")
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
mux "github.com/dereulenspiegel/coap-mux"
|
||||
coap "github.com/dustin/go-coap"
|
||||
"github.com/mainflux/mainflux/writer"
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
func (ca *CoAPAdapter) sendMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Message) *coap.Message {
|
||||
@ -34,14 +34,14 @@ func (ca *CoAPAdapter) sendMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Messa
|
||||
// Channel ID
|
||||
cid := mux.Var(m, "channel_id")
|
||||
|
||||
n := writer.RawMessage{
|
||||
n := mainflux.RawMessage{
|
||||
Channel: cid,
|
||||
Publisher: "",
|
||||
Protocol: protocol,
|
||||
Payload: m.Payload,
|
||||
}
|
||||
|
||||
if err := ca.repo.Save(n); err != nil {
|
||||
if err := ca.pub.Publish(n); err != nil {
|
||||
if m.IsConfirmable() {
|
||||
res.Code = coap.InternalServerError
|
||||
}
|
||||
@ -123,7 +123,7 @@ func (ca *CoAPAdapter) observeMessage(l *net.UDPConn, a *net.UDPAddr, m *coap.Me
|
||||
return res
|
||||
}
|
||||
|
||||
func (ca *CoAPAdapter) obsTransmit(n writer.RawMessage) {
|
||||
func (ca *CoAPAdapter) obsTransmit(n mainflux.RawMessage) {
|
||||
for _, v := range ca.obsMap[n.Channel] {
|
||||
msg := *(v.message)
|
||||
msg.Payload = n.Payload
|
||||
|
@ -4,31 +4,28 @@ package nats
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mainflux/mainflux/writer"
|
||||
"github.com/mainflux/mainflux"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const topic string = "msg.coap"
|
||||
const topic string = "src.coap"
|
||||
|
||||
var _ writer.MessageRepository = (*natsRepository)(nil)
|
||||
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
|
||||
|
||||
type natsRepository struct {
|
||||
type natsPublisher struct {
|
||||
nc *broker.Conn
|
||||
}
|
||||
|
||||
// NewMessageRepository instantiates NATS message repository. Note that the
|
||||
// repository will not truly persist messages, but instead they will be
|
||||
// published to the topic and made available for persisting by all interested
|
||||
// parties, i.e. the message-writer service.
|
||||
func NewMessageRepository(nc *broker.Conn) writer.MessageRepository {
|
||||
return &natsRepository{nc}
|
||||
// NewMessagePublisher instantiates NATS message publisher.
|
||||
func NewMessagePublisher(nc *broker.Conn) mainflux.MessagePublisher {
|
||||
return &natsPublisher{nc}
|
||||
}
|
||||
|
||||
func (repo *natsRepository) Save(msg writer.RawMessage) error {
|
||||
b, err := json.Marshal(msg)
|
||||
func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return repo.nc.Publish(topic, b)
|
||||
return pub.nc.Publish(topic, data)
|
||||
}
|
||||
|
3
doc.go
Normal file
3
doc.go
Normal file
@ -0,0 +1,3 @@
|
||||
// Package mainflux acts as an umbrella package containing multiple different
|
||||
// microservices and defines all shared domain concepts.
|
||||
package mainflux
|
3
docker/.dockerignore
Normal file
3
docker/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
docker-compose.yml
|
||||
nginx.conf
|
||||
ssl/
|
11
docker/Dockerfile
Normal file
11
docker/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
||||
FROM golang:1.9-alpine AS builder
|
||||
ARG SVC_NAME
|
||||
RUN apk update \
|
||||
&& apk add git \
|
||||
&& go get github.com/mainflux/mainflux \
|
||||
&& cd /go/src/github.com/mainflux/mainflux/cmd/$SVC_NAME \
|
||||
&& CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o /exe
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /exe /
|
||||
ENTRYPOINT ["/exe"]
|
78
docker/docker-compose.yml
Normal file
78
docker/docker-compose.yml
Normal file
@ -0,0 +1,78 @@
|
||||
###
|
||||
# Copyright (c) 2015-2017 Mainflux
|
||||
#
|
||||
# Mainflux is licensed under an Apache license, version 2.0 license.
|
||||
# All rights not explicitly granted in the Apache license, version 2.0 are reserved.
|
||||
# See the included LICENSE file for more details.
|
||||
###
|
||||
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:1.13-alpine
|
||||
container_name: mainflux-nginx
|
||||
network_mode: bridge
|
||||
volumes:
|
||||
- $PWD/nginx.conf:/etc/nginx/nginx.conf
|
||||
- $PWD/ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
|
||||
- $PWD/ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
|
||||
- $PWD/ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8883:8883"
|
||||
|
||||
nats:
|
||||
image: nats:1.0.2
|
||||
container_name: mainflux-nats
|
||||
network_mode: bridge
|
||||
|
||||
postgres:
|
||||
image: postgres:10.2-alpine
|
||||
container_name: mainflux-postgres
|
||||
network_mode: bridge
|
||||
environment:
|
||||
POSTGRES_USER: mainflux
|
||||
POSTGRES_PASSWORD: mainflux
|
||||
POSTGRES_DB: mainflux
|
||||
|
||||
manager:
|
||||
image: mainflux/manager:latest
|
||||
container_name: mainflux-manager
|
||||
network_mode: bridge
|
||||
environment:
|
||||
MF_DB_HOST: postgres
|
||||
MF_MANAGER_DB: mainflux
|
||||
MF_MANAGER_PORT: 8180
|
||||
MF_MANAGER_SECRET: test-secret
|
||||
|
||||
normalizer:
|
||||
image: mainflux/normalizer:latest
|
||||
container_name: mainflux-normalizer
|
||||
network_mode: bridge
|
||||
environment:
|
||||
MF_NATS_URL: "nats://nats:4222"
|
||||
MF_NORMALIZER_PORT: 8181
|
||||
|
||||
http-adapter:
|
||||
image: mainflux/http:latest
|
||||
container_name: mainflux-http
|
||||
network_mode: bridge
|
||||
depends_on:
|
||||
- manager
|
||||
environment:
|
||||
MF_MANAGER_URL: "http://manager:8180"
|
||||
MF_NATS_URL: "nats://nats:4222"
|
||||
MF_HTTP_ADAPTER_PORT: 8182
|
||||
|
||||
mqtt-adapter:
|
||||
image: mainflux/mqtt-adapter:latest
|
||||
container_name: mainflux-mqtt
|
||||
network_mode: bridge
|
||||
depends_on:
|
||||
- manager
|
||||
environment:
|
||||
MQTT_ADAPTER_NATS_URL: "nats://nats:4222"
|
||||
AUTH_URL: "http://manager"
|
||||
AUTH_PORT: 8180
|
@ -17,8 +17,8 @@ pid /run/nginx.pid;
|
||||
include /etc/nginx/modules-enabled/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 768;
|
||||
# multi_accept on;
|
||||
worker_connections 768;
|
||||
# multi_accept on;
|
||||
}
|
||||
|
||||
###
|
||||
@ -26,44 +26,44 @@ events {
|
||||
###
|
||||
http {
|
||||
|
||||
##
|
||||
# Basic Settings
|
||||
##
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
# server_tokens off;
|
||||
##
|
||||
# Basic Settings
|
||||
##
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
# server_tokens off;
|
||||
|
||||
# server_names_hash_bucket_size 64;
|
||||
# server_name_in_redirect off;
|
||||
# server_names_hash_bucket_size 64;
|
||||
# server_name_in_redirect off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
##
|
||||
# SSL Settings
|
||||
##
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
|
||||
ssl_prefer_server_ciphers on;
|
||||
##
|
||||
# SSL Settings
|
||||
##
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
##
|
||||
# Logging Settings
|
||||
##
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
##
|
||||
# Logging Settings
|
||||
##
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
upstream docker-manager {
|
||||
server mainflux-manager:8180;
|
||||
}
|
||||
upstream docker-http {
|
||||
server mainflux-http:7070;
|
||||
server mainflux-http:8182;
|
||||
}
|
||||
|
||||
##
|
||||
# Virtual Host Configs
|
||||
##
|
||||
##
|
||||
# Virtual Host Configs
|
||||
##
|
||||
|
||||
# HTTP
|
||||
server {
|
||||
@ -124,8 +124,9 @@ http {
|
||||
|
||||
|
||||
server_name localhost;
|
||||
|
||||
# Proxy pass to manager service
|
||||
location /api/ {
|
||||
location / {
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@ -140,8 +141,9 @@ http {
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy pass to mainflux-http-adapter
|
||||
location /pub/ {
|
||||
location ~ ^/channels/[a-zA-Z0-9-]+/messages$ {
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
@ -1,7 +1,6 @@
|
||||
# Mainflux HTTP adapter
|
||||
# HTTP adapter
|
||||
|
||||
Mainflux HTTP adapter provides an HTTP API for sending messages through the
|
||||
platform.
|
||||
HTTP adapter provides an HTTP API for sending messages through the platform.
|
||||
|
||||
## Configuration
|
||||
|
||||
@ -9,9 +8,11 @@ The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|-----------------------|-------------------|-----------------------|
|
||||
| HTTP_ADAPTER_NATS_URL | NATS instance URL | nats://localhost:4222 |
|
||||
| Variable | Description | Default |
|
||||
|----------------------|---------------------|-----------------------|
|
||||
| MF_MANAGER_URL | Manager service URL | http://localhost:8180 |
|
||||
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
|
||||
| MF_HTTP_ADAPTER_PORT | Service HTTP port | 8180 |
|
||||
|
||||
## Deployment
|
||||
|
||||
@ -22,12 +23,14 @@ a compose file template that can be used to deploy the service container locally
|
||||
version: "2"
|
||||
services:
|
||||
adapter:
|
||||
image: mainflux/http-adapter:[version]
|
||||
image: mainflux/http:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:8180
|
||||
environment:
|
||||
HTTP_ADAPTER_NATS_URL: [NATS instance URL]
|
||||
MF_MANAGER_URL: [Manager service URL]
|
||||
MF_NATS_URL: [NATS instance URL]
|
||||
MF_HTTP_ADAPTER_PORT: [Service HTTP port]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
@ -42,7 +45,7 @@ cd $GOPATH/src/github.com/mainflux/mainflux/cmd/http
|
||||
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
|
||||
|
||||
# set the environment variables and run the service
|
||||
HTTP_ADAPTER_NATS_URL=[NATS instance URL] app
|
||||
MF_MANAGER_URL=[Manager service URL] MF_NATS_URL=[NATS instance URL] MF_HTTP_ADAPTER_PORT=[Service HTTP port] app
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
@ -1,18 +1,18 @@
|
||||
package http
|
||||
|
||||
import "github.com/mainflux/mainflux/writer"
|
||||
import "github.com/mainflux/mainflux"
|
||||
|
||||
var _ Service = (*adapterService)(nil)
|
||||
var _ mainflux.MessagePublisher = (*adapterService)(nil)
|
||||
|
||||
type adapterService struct {
|
||||
mr writer.MessageRepository
|
||||
pub mainflux.MessagePublisher
|
||||
}
|
||||
|
||||
// NewService instantiates the domain service implementation.
|
||||
func NewService(mr writer.MessageRepository) Service {
|
||||
return &adapterService{mr}
|
||||
// New instantiates the domain service implementation.
|
||||
func New(pub mainflux.MessagePublisher) mainflux.MessagePublisher {
|
||||
return &adapterService{pub}
|
||||
}
|
||||
|
||||
func (as *adapterService) Publish(msg writer.RawMessage) error {
|
||||
return as.mr.Save(msg)
|
||||
func (as *adapterService) Publish(msg mainflux.RawMessage) error {
|
||||
return as.pub.Publish(msg)
|
||||
}
|
||||
|
@ -4,13 +4,12 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/writer"
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
func sendMessageEndpoint(svc http.Service) endpoint.Endpoint {
|
||||
func sendMessageEndpoint(svc mainflux.MessagePublisher) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
msg := request.(writer.RawMessage)
|
||||
msg := request.(mainflux.RawMessage)
|
||||
err := svc.Publish(msg)
|
||||
return nil, err
|
||||
}
|
||||
|
@ -4,29 +4,28 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/writer"
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var _ http.Service = (*loggingService)(nil)
|
||||
var _ mainflux.MessagePublisher = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingService struct {
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
http.Service
|
||||
svc mainflux.MessagePublisher
|
||||
}
|
||||
|
||||
// NewLoggingService adds logging facilities to the adapter.
|
||||
func NewLoggingService(logger log.Logger, s http.Service) http.Service {
|
||||
return &loggingService{logger, s}
|
||||
// LoggingMiddleware adds logging facilities to the adapter.
|
||||
func LoggingMiddleware(svc mainflux.MessagePublisher, logger log.Logger) mainflux.MessagePublisher {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (ls *loggingService) Publish(msg writer.RawMessage) error {
|
||||
func (lm *loggingMiddleware) Publish(msg mainflux.RawMessage) error {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "publish",
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.Publish(msg)
|
||||
return lm.svc.Publish(msg)
|
||||
}
|
||||
|
@ -4,32 +4,31 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/mainflux/mainflux/http"
|
||||
"github.com/mainflux/mainflux/writer"
|
||||
"github.com/mainflux/mainflux"
|
||||
)
|
||||
|
||||
var _ http.Service = (*metricService)(nil)
|
||||
var _ mainflux.MessagePublisher = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricService struct {
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
http.Service
|
||||
svc mainflux.MessagePublisher
|
||||
}
|
||||
|
||||
// NewMetricService instruments adapter by tracking request count and latency.
|
||||
func NewMetricService(counter metrics.Counter, latency metrics.Histogram, s http.Service) http.Service {
|
||||
return &metricService{
|
||||
// MetricsMiddleware instruments adapter by tracking request count and latency.
|
||||
func MetricsMiddleware(svc mainflux.MessagePublisher, counter metrics.Counter, latency metrics.Histogram) mainflux.MessagePublisher {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
Service: s,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricService) Publish(msg writer.RawMessage) error {
|
||||
func (mm *metricsMiddleware) Publish(msg mainflux.RawMessage) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "publish").Add(1)
|
||||
ms.latency.With("method", "publish").Observe(time.Since(begin).Seconds())
|
||||
mm.counter.With("method", "publish").Add(1)
|
||||
mm.latency.With("method", "publish").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.Publish(msg)
|
||||
return mm.svc.Publish(msg)
|
||||
}
|
||||
|
@ -11,16 +11,11 @@ import (
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
adapter "github.com/mainflux/mainflux/http"
|
||||
manager "github.com/mainflux/mainflux/manager/client"
|
||||
"github.com/mainflux/mainflux/writer"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
protocol string = "http"
|
||||
ctJson string = "application/senml+json"
|
||||
)
|
||||
const protocol string = "http"
|
||||
|
||||
var (
|
||||
errMalformedData error = errors.New("malformed SenML data")
|
||||
@ -30,7 +25,7 @@ var (
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc adapter.Service, mc manager.ManagerClient) http.Handler {
|
||||
func MakeHandler(svc mainflux.MessagePublisher, mc manager.ManagerClient) http.Handler {
|
||||
auth = mc
|
||||
|
||||
opts := []kithttp.ServerOption{
|
||||
@ -53,11 +48,6 @@ func MakeHandler(svc adapter.Service, mc manager.ManagerClient) http.Handler {
|
||||
}
|
||||
|
||||
func decodeRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
ct, err := checkContentType(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publisher, err := authorize(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -68,13 +58,11 @@ func decodeRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channel := bone.GetValue(r, "id")
|
||||
|
||||
msg := writer.RawMessage{
|
||||
msg := mainflux.RawMessage{
|
||||
Publisher: publisher,
|
||||
Protocol: protocol,
|
||||
ContentType: ct,
|
||||
Channel: channel,
|
||||
ContentType: r.Header.Get("Content-Type"),
|
||||
Channel: bone.GetValue(r, "id"),
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
@ -88,7 +76,7 @@ func authorize(r *http.Request) (string, error) {
|
||||
return "", errUnauthorizedAccess
|
||||
}
|
||||
|
||||
// Path is `/channels/:id/messages`, we need chanID.
|
||||
// extract ID from /channels/:id/messages
|
||||
c := strings.Split(r.URL.Path, "/")[2]
|
||||
|
||||
id, err := auth.CanAccess(c, apiKey)
|
||||
@ -99,16 +87,6 @@ func authorize(r *http.Request) (string, error) {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func checkContentType(r *http.Request) (string, error) {
|
||||
ct := r.Header.Get("Content-Type")
|
||||
|
||||
if ct != ctJson {
|
||||
return "", errUnknownType
|
||||
}
|
||||
|
||||
return ct, nil
|
||||
}
|
||||
|
||||
func decodePayload(body io.ReadCloser) ([]byte, error) {
|
||||
payload, err := ioutil.ReadAll(body)
|
||||
if err != nil {
|
||||
|
@ -1,34 +0,0 @@
|
||||
// Package nats contains NATS-specific message repository implementation.
|
||||
package nats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mainflux/mainflux/writer"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const topic string = "msg.http"
|
||||
|
||||
var _ writer.MessageRepository = (*natsRepository)(nil)
|
||||
|
||||
type natsRepository struct {
|
||||
nc *broker.Conn
|
||||
}
|
||||
|
||||
// NewMessageRepository instantiates NATS message repository. Note that the
|
||||
// repository will not truly persist messages, but instead they will be
|
||||
// published to the topic and made available for persisting by all interested
|
||||
// parties, i.e. the message-writer service.
|
||||
func NewMessageRepository(nc *broker.Conn) writer.MessageRepository {
|
||||
return &natsRepository{nc}
|
||||
}
|
||||
|
||||
func (repo *natsRepository) Save(msg writer.RawMessage) error {
|
||||
b, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return repo.nc.Publish(topic, b)
|
||||
}
|
31
http/nats/publisher.go
Normal file
31
http/nats/publisher.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Package nats contains NATS message publisher implementation.
|
||||
package nats
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mainflux/mainflux"
|
||||
broker "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const topic string = "src.http"
|
||||
|
||||
var _ mainflux.MessagePublisher = (*natsPublisher)(nil)
|
||||
|
||||
type natsPublisher struct {
|
||||
nc *broker.Conn
|
||||
}
|
||||
|
||||
// NewMessagePublisher instantiates NATS message publisher.
|
||||
func NewMessagePublisher(nc *broker.Conn) mainflux.MessagePublisher {
|
||||
return &natsPublisher{nc}
|
||||
}
|
||||
|
||||
func (pub *natsPublisher) Publish(msg mainflux.RawMessage) error {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pub.nc.Publish(topic, data)
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package http
|
||||
|
||||
import "github.com/mainflux/mainflux/writer"
|
||||
|
||||
// Service specifies an API that must be fullfiled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// Publish accepts the raw SenML message and publishes it to the event bus
|
||||
// for post processing.
|
||||
Publish(writer.RawMessage) error
|
||||
}
|
@ -14,6 +14,7 @@ paths:
|
||||
- messages
|
||||
consumes:
|
||||
- "application/senml+json"
|
||||
- "text/plain"
|
||||
produces: []
|
||||
parameters:
|
||||
- name: Authorization
|
||||
@ -28,11 +29,15 @@ paths:
|
||||
format: uuid
|
||||
required: true
|
||||
- name: message
|
||||
description: Message to be sent.
|
||||
description: |
|
||||
Message to be distributed. Since the platform expects messages to be
|
||||
properly formatted SenML in order to be post-processed, clients are
|
||||
obliged to specify Content-Type header for each published message.
|
||||
Note that all messages that aren't SenML will be accepted and published,
|
||||
but no post-processing will be applied.
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
type: string
|
||||
responses:
|
||||
202:
|
||||
description: Message is accepted for processing.
|
||||
@ -41,66 +46,6 @@ paths:
|
||||
403:
|
||||
description: Message discarded due to missing or invalid credentials.
|
||||
415:
|
||||
description: Message discarded due to invalid content type.
|
||||
description: Message discarded due to invalid or missing content type.
|
||||
500:
|
||||
description: Unexpected server-side error occured.
|
||||
|
||||
definitions:
|
||||
Message:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
bn:
|
||||
type: string
|
||||
description: Base name
|
||||
bt:
|
||||
type: number
|
||||
format: double
|
||||
description: Base time
|
||||
bu:
|
||||
type: string
|
||||
description: Base unit
|
||||
bv:
|
||||
type: number
|
||||
format: double
|
||||
description: Base value
|
||||
bver:
|
||||
type: integer
|
||||
description: Base version
|
||||
n:
|
||||
type: string
|
||||
description: Name
|
||||
u:
|
||||
type: string
|
||||
description: Unit
|
||||
v:
|
||||
type: number
|
||||
format: double
|
||||
description: Value
|
||||
vs:
|
||||
type: string
|
||||
description: String value
|
||||
vb:
|
||||
type: boolean
|
||||
description: Boolean value
|
||||
vd:
|
||||
type: string
|
||||
description: Data value
|
||||
s:
|
||||
type: number
|
||||
format: double
|
||||
description: Value sum
|
||||
t:
|
||||
type: number
|
||||
format: double
|
||||
description: Time
|
||||
ut:
|
||||
type: number
|
||||
format: double
|
||||
description: Update time
|
||||
l:
|
||||
type: string
|
||||
description: Link
|
||||
|
@ -1,8 +1,8 @@
|
||||
# Mainflux manager
|
||||
# Manager
|
||||
|
||||
Mainflux manager provides an HTTP API for managing platform resources: users,
|
||||
devices, applications and channels. Through this API clients are able to do
|
||||
the following actions:
|
||||
Manager provides an HTTP API for managing platform resources: users, devices,
|
||||
applications and channels. Through this API clients are able to do the following
|
||||
actions:
|
||||
|
||||
- register new accounts and obtain access tokens
|
||||
- provision new clients (i.e. devices & applications)
|
||||
@ -18,21 +18,18 @@ The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---------------------|------------------------------------------|-----------|
|
||||
| MANAGER_DB_CLUSTER | comma-separated Cassandra contact points | 127.0.0.1 |
|
||||
| MANAGER_DB_KEYSPACE | name of the Cassandra keyspace | manager |
|
||||
| MANAGER_SECRET | string used for signing tokens | manager |
|
||||
| Variable | Description | Default |
|
||||
|-------------------|------------------------------------------|-----------|
|
||||
| MF_DB_HOST | Database host address | localhost |
|
||||
| MF_DB_PORT | Database host port | 5432 |
|
||||
| MF_DB_USER | Database user | mainflux |
|
||||
| MF_DB_PASSWORD | Database password | mainflux |
|
||||
| MF_MANAGER_DB | Name of the database used by the service | manager |
|
||||
| MF_MANAGER_PORT | Manager service HTTP port | 8180 |
|
||||
| MF_MANAGER_SECRET | string used for signing tokens | manager |
|
||||
|
||||
## Deployment
|
||||
|
||||
Before proceeding to deployment, make sure to check out the [Apache Cassandra 3.0.x
|
||||
documentation][www:cassandra]. Developers are advised to get acquainted with
|
||||
basic architectural concepts, data modeling techniques and deployment strategies.
|
||||
|
||||
> Prior to deploying the service, make sure to set up the database and create
|
||||
the keyspace that will be used by the service.
|
||||
|
||||
The service itself is distributed as Docker container. The following snippet
|
||||
provides a compose file template that can be used to deploy the service container
|
||||
locally:
|
||||
@ -44,11 +41,15 @@ services:
|
||||
image: mainflux/manager:[version]
|
||||
container_name: [instance name]
|
||||
ports:
|
||||
- [host machine port]:8180
|
||||
- [host machine port]:[configured HTTP port]
|
||||
environment:
|
||||
MANAGER_DB_CLUSTER: [comma-separated Cassandra endpoints]
|
||||
MANAGER_DB_KEYSPACE: [name of Cassandra keyspace]
|
||||
MANAGER_SECRET: [string used for signing tokens]
|
||||
MF_DB_HOST: [Database host address]
|
||||
MF_DB_PORT: [Database host port]
|
||||
MF_DB_USER: [Database user]
|
||||
MF_DB_PASS: [Database password]
|
||||
MF_MANAGER_DB: [Name of the database used by the service]
|
||||
MF_MANAGER_PORT: [Service HTTP port]
|
||||
MF_MANAGER_SECRET: [String used for signing tokens]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
@ -63,7 +64,7 @@ cd $GOPATH/src/github.com/mainflux/mainflux/cmd/manager
|
||||
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
|
||||
|
||||
# set the environment variables and run the service
|
||||
MANAGER_DB_CLUSTER=[comma-separated Cassandra endpoints] MANAGER_DB_KEYSPACE=[name of Cassandra keyspace] MANAGER_SECRET=[string used for signing tokens] app
|
||||
MF_DB_HOST=[Database host address] MF_DB_PORT=[Database host port] MF_DB_USER=[Database user] MF_DB_PASS=[Database password] MF_MANAGER_DB=[Name of the database used by the service] MF_MANAGER_PORT=[Service HTTP port] MF_MANAGER_SECRET=[String used for signing tokens] app
|
||||
```
|
||||
|
||||
## Usage
|
||||
@ -72,4 +73,3 @@ For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](swagger.yaml).
|
||||
|
||||
[doc]: http://mainflux.readthedocs.io
|
||||
[www:cassandra]: http://docs.datastax.com
|
||||
|
@ -200,22 +200,51 @@ func removeChannelEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(viewResourceReq)
|
||||
|
||||
err := req.validate()
|
||||
if err == manager.ErrNotFound {
|
||||
return removeRes{}, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err := req.validate(); err != nil {
|
||||
if err == manager.ErrNotFound {
|
||||
return removeRes{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = svc.RemoveChannel(req.key, req.id); err != nil {
|
||||
if err := svc.RemoveChannel(req.key, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
func connectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
cr := request.(connectionReq)
|
||||
|
||||
if err := cr.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Connect(cr.key, cr.chanId, cr.clientId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return connectionRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func disconnectEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
cr := request.(connectionReq)
|
||||
|
||||
if err := cr.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.Disconnect(cr.key, cr.chanId, cr.clientId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return disconnectionRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func identityEndpoint(svc manager.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
|
@ -7,21 +7,21 @@ import (
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.Service = (*loggingService)(nil)
|
||||
var _ manager.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingService struct {
|
||||
type loggingMiddleware struct {
|
||||
logger log.Logger
|
||||
manager.Service
|
||||
svc manager.Service
|
||||
}
|
||||
|
||||
// NewLoggingService adds logging facilities to the core service.
|
||||
func NewLoggingService(logger log.Logger, s manager.Service) manager.Service {
|
||||
return &loggingService{logger, s}
|
||||
// LoggingMiddleware adds logging facilities to the core service.
|
||||
func LoggingMiddleware(svc manager.Service, logger log.Logger) manager.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (ls *loggingService) Register(user manager.User) (err error) {
|
||||
func (lm *loggingMiddleware) Register(user manager.User) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "register",
|
||||
"email", user.Email,
|
||||
"error", err,
|
||||
@ -29,12 +29,12 @@ func (ls *loggingService) Register(user manager.User) (err error) {
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.Register(user)
|
||||
return lm.svc.Register(user)
|
||||
}
|
||||
|
||||
func (ls *loggingService) Login(user manager.User) (token string, err error) {
|
||||
func (lm *loggingMiddleware) Login(user manager.User) (token string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "login",
|
||||
"email", user.Email,
|
||||
"error", err,
|
||||
@ -42,12 +42,12 @@ func (ls *loggingService) Login(user manager.User) (token string, err error) {
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.Login(user)
|
||||
return lm.svc.Login(user)
|
||||
}
|
||||
|
||||
func (ls *loggingService) AddClient(key string, client manager.Client) (id string, err error) {
|
||||
func (lm *loggingMiddleware) AddClient(key string, client manager.Client) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "add_client",
|
||||
"key", key,
|
||||
"id", id,
|
||||
@ -56,12 +56,12 @@ func (ls *loggingService) AddClient(key string, client manager.Client) (id strin
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.AddClient(key, client)
|
||||
return lm.svc.AddClient(key, client)
|
||||
}
|
||||
|
||||
func (ls *loggingService) UpdateClient(key string, client manager.Client) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateClient(key string, client manager.Client) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "update_client",
|
||||
"key", key,
|
||||
"id", client.ID,
|
||||
@ -70,12 +70,12 @@ func (ls *loggingService) UpdateClient(key string, client manager.Client) (err e
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.UpdateClient(key, client)
|
||||
return lm.svc.UpdateClient(key, client)
|
||||
}
|
||||
|
||||
func (ls *loggingService) ViewClient(key string, id string) (client manager.Client, err error) {
|
||||
func (lm *loggingMiddleware) ViewClient(key string, id string) (client manager.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "view_client",
|
||||
"key", key,
|
||||
"id", id,
|
||||
@ -84,12 +84,12 @@ func (ls *loggingService) ViewClient(key string, id string) (client manager.Clie
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.ViewClient(key, id)
|
||||
return lm.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (ls *loggingService) ListClients(key string) (clients []manager.Client, err error) {
|
||||
func (lm *loggingMiddleware) ListClients(key string) (clients []manager.Client, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "list_clients",
|
||||
"key", key,
|
||||
"error", err,
|
||||
@ -97,12 +97,12 @@ func (ls *loggingService) ListClients(key string) (clients []manager.Client, err
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.ListClients(key)
|
||||
return lm.svc.ListClients(key)
|
||||
}
|
||||
|
||||
func (ls *loggingService) RemoveClient(key string, id string) (err error) {
|
||||
func (lm *loggingMiddleware) RemoveClient(key string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "remove_client",
|
||||
"key", key,
|
||||
"id", id,
|
||||
@ -111,12 +111,12 @@ func (ls *loggingService) RemoveClient(key string, id string) (err error) {
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.RemoveClient(key, id)
|
||||
return lm.svc.RemoveClient(key, id)
|
||||
}
|
||||
|
||||
func (ls *loggingService) CreateChannel(key string, channel manager.Channel) (id string, err error) {
|
||||
func (lm *loggingMiddleware) CreateChannel(key string, channel manager.Channel) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "create_channel",
|
||||
"key", key,
|
||||
"id", id,
|
||||
@ -125,12 +125,12 @@ func (ls *loggingService) CreateChannel(key string, channel manager.Channel) (id
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.CreateChannel(key, channel)
|
||||
return lm.svc.CreateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ls *loggingService) UpdateChannel(key string, channel manager.Channel) (err error) {
|
||||
func (lm *loggingMiddleware) UpdateChannel(key string, channel manager.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "update_channel",
|
||||
"key", key,
|
||||
"id", channel.ID,
|
||||
@ -139,12 +139,12 @@ func (ls *loggingService) UpdateChannel(key string, channel manager.Channel) (er
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.UpdateChannel(key, channel)
|
||||
return lm.svc.UpdateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ls *loggingService) ViewChannel(key string, id string) (channel manager.Channel, err error) {
|
||||
func (lm *loggingMiddleware) ViewChannel(key string, id string) (channel manager.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "view_channel",
|
||||
"key", key,
|
||||
"id", id,
|
||||
@ -153,12 +153,12 @@ func (ls *loggingService) ViewChannel(key string, id string) (channel manager.Ch
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.ViewChannel(key, id)
|
||||
return lm.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (ls *loggingService) ListChannels(key string) (channels []manager.Channel, err error) {
|
||||
func (lm *loggingMiddleware) ListChannels(key string) (channels []manager.Channel, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "list_channels",
|
||||
"key", key,
|
||||
"error", err,
|
||||
@ -166,12 +166,12 @@ func (ls *loggingService) ListChannels(key string) (channels []manager.Channel,
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.ListChannels(key)
|
||||
return lm.svc.ListChannels(key)
|
||||
}
|
||||
|
||||
func (ls *loggingService) RemoveChannel(key string, id string) (err error) {
|
||||
func (lm *loggingMiddleware) RemoveChannel(key string, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "remove_channel",
|
||||
"key", key,
|
||||
"id", id,
|
||||
@ -180,12 +180,42 @@ func (ls *loggingService) RemoveChannel(key string, id string) (err error) {
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.RemoveChannel(key, id)
|
||||
return lm.svc.RemoveChannel(key, id)
|
||||
}
|
||||
|
||||
func (ls *loggingService) Identity(key string) (id string, err error) {
|
||||
func (lm *loggingMiddleware) Connect(key, chanId, clientId string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "connect",
|
||||
"key", key,
|
||||
"channel", chanId,
|
||||
"client", clientId,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Connect(key, chanId, clientId)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Disconnect(key, chanId, clientId string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "disconnect",
|
||||
"key", key,
|
||||
"channel", chanId,
|
||||
"client", clientId,
|
||||
"error", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Disconnect(key, chanId, clientId)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Identity(key string) (id string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
lm.logger.Log(
|
||||
"method", "identity",
|
||||
"id", id,
|
||||
"error", err,
|
||||
@ -193,12 +223,12 @@ func (ls *loggingService) Identity(key string) (id string, err error) {
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.Identity(key)
|
||||
return lm.svc.Identity(key)
|
||||
}
|
||||
|
||||
func (ls *loggingService) CanAccess(key string, id string) (pub string, err error) {
|
||||
func (lm *loggingMiddleware) CanAccess(key string, id string) (pub string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
ls.logger.Log(
|
||||
lm.logger.Log(
|
||||
"method", "can_access",
|
||||
"key", key,
|
||||
"id", id,
|
||||
@ -208,5 +238,5 @@ func (ls *loggingService) CanAccess(key string, id string) (pub string, err erro
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
return ls.Service.CanAccess(key, id)
|
||||
return lm.svc.CanAccess(key, id)
|
||||
}
|
||||
|
@ -7,146 +7,164 @@ import (
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.Service = (*metricService)(nil)
|
||||
var _ manager.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricService struct {
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
manager.Service
|
||||
svc manager.Service
|
||||
}
|
||||
|
||||
// NewMetricService instruments core service by tracking request count and
|
||||
// MetricsMiddleware instruments core service by tracking request count and
|
||||
// latency.
|
||||
func NewMetricService(counter metrics.Counter, latency metrics.Histogram, s manager.Service) manager.Service {
|
||||
return &metricService{
|
||||
func MetricsMiddleware(svc manager.Service, counter metrics.Counter, latency metrics.Histogram) manager.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
Service: s,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricService) Register(user manager.User) error {
|
||||
func (ms *metricsMiddleware) Register(user manager.User) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "register").Add(1)
|
||||
ms.latency.With("method", "register").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.Register(user)
|
||||
return ms.svc.Register(user)
|
||||
}
|
||||
|
||||
func (ms *metricService) Login(user manager.User) (string, error) {
|
||||
func (ms *metricsMiddleware) Login(user manager.User) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "login").Add(1)
|
||||
ms.latency.With("method", "login").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.Login(user)
|
||||
return ms.svc.Login(user)
|
||||
}
|
||||
|
||||
func (ms *metricService) AddClient(key string, client manager.Client) (string, error) {
|
||||
func (ms *metricsMiddleware) AddClient(key string, client manager.Client) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "add_client").Add(1)
|
||||
ms.latency.With("method", "add_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.AddClient(key, client)
|
||||
return ms.svc.AddClient(key, client)
|
||||
}
|
||||
|
||||
func (ms *metricService) UpdateClient(key string, client manager.Client) error {
|
||||
func (ms *metricsMiddleware) UpdateClient(key string, client manager.Client) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_client").Add(1)
|
||||
ms.latency.With("method", "update_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.UpdateClient(key, client)
|
||||
return ms.svc.UpdateClient(key, client)
|
||||
}
|
||||
|
||||
func (ms *metricService) ViewClient(key string, id string) (manager.Client, error) {
|
||||
func (ms *metricsMiddleware) ViewClient(key string, id string) (manager.Client, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_client").Add(1)
|
||||
ms.latency.With("method", "view_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.ViewClient(key, id)
|
||||
return ms.svc.ViewClient(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricService) ListClients(key string) ([]manager.Client, error) {
|
||||
func (ms *metricsMiddleware) ListClients(key string) ([]manager.Client, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_clients").Add(1)
|
||||
ms.latency.With("method", "list_clients").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.ListClients(key)
|
||||
return ms.svc.ListClients(key)
|
||||
}
|
||||
|
||||
func (ms *metricService) RemoveClient(key string, id string) error {
|
||||
func (ms *metricsMiddleware) RemoveClient(key string, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_client").Add(1)
|
||||
ms.latency.With("method", "remove_client").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.RemoveClient(key, id)
|
||||
return ms.svc.RemoveClient(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricService) CreateChannel(key string, channel manager.Channel) (string, error) {
|
||||
func (ms *metricsMiddleware) CreateChannel(key string, channel manager.Channel) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "create_channel").Add(1)
|
||||
ms.latency.With("method", "create_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.CreateChannel(key, channel)
|
||||
return ms.svc.CreateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ms *metricService) UpdateChannel(key string, channel manager.Channel) error {
|
||||
func (ms *metricsMiddleware) UpdateChannel(key string, channel manager.Channel) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "update_channel").Add(1)
|
||||
ms.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.UpdateChannel(key, channel)
|
||||
return ms.svc.UpdateChannel(key, channel)
|
||||
}
|
||||
|
||||
func (ms *metricService) ViewChannel(key string, id string) (manager.Channel, error) {
|
||||
func (ms *metricsMiddleware) ViewChannel(key string, id string) (manager.Channel, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "view_channel").Add(1)
|
||||
ms.latency.With("method", "view_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.ViewChannel(key, id)
|
||||
return ms.svc.ViewChannel(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricService) ListChannels(key string) ([]manager.Channel, error) {
|
||||
func (ms *metricsMiddleware) ListChannels(key string) ([]manager.Channel, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_channels").Add(1)
|
||||
ms.latency.With("method", "list_channels").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.ListChannels(key)
|
||||
return ms.svc.ListChannels(key)
|
||||
}
|
||||
|
||||
func (ms *metricService) RemoveChannel(key string, id string) error {
|
||||
func (ms *metricsMiddleware) RemoveChannel(key string, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "remove_channel").Add(1)
|
||||
ms.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.RemoveChannel(key, id)
|
||||
return ms.svc.RemoveChannel(key, id)
|
||||
}
|
||||
|
||||
func (ms *metricService) Identity(key string) (string, error) {
|
||||
func (ms *metricsMiddleware) Connect(key, chanId, clientId string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "connect").Add(1)
|
||||
ms.latency.With("method", "connect").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Connect(key, chanId, clientId)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Disconnect(key, chanId, clientId string) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "disconnect").Add(1)
|
||||
ms.latency.With("method", "disconnect").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.Disconnect(key, chanId, clientId)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) Identity(key string) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "identity").Add(1)
|
||||
ms.latency.With("method", "identity").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.Identity(key)
|
||||
return ms.svc.Identity(key)
|
||||
}
|
||||
|
||||
func (ms *metricService) CanAccess(key string, id string) (string, error) {
|
||||
func (ms *metricsMiddleware) CanAccess(key string, id string) (string, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "can_access").Add(1)
|
||||
ms.latency.With("method", "can_access").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.Service.CanAccess(key, id)
|
||||
return ms.svc.CanAccess(key, id)
|
||||
}
|
||||
|
@ -125,3 +125,21 @@ func (req listResourcesReq) validate() error {
|
||||
|
||||
return manager.ErrMalformedEntity
|
||||
}
|
||||
|
||||
type connectionReq struct {
|
||||
key string
|
||||
chanId string
|
||||
clientId string
|
||||
}
|
||||
|
||||
func (req connectionReq) validate() error {
|
||||
if req.key == "" {
|
||||
return manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
if !govalidator.IsUUID(req.chanId) && !govalidator.IsUUID(req.clientId) {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -4,88 +4,93 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const wrong string = "?"
|
||||
|
||||
var (
|
||||
client manager.Client = manager.Client{Type: "app"}
|
||||
channel manager.Channel = manager.Channel{}
|
||||
)
|
||||
|
||||
func TestUserReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
cases := map[string]struct {
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
{manager.User{"foo@example.com", "pass"}, nil},
|
||||
{manager.User{"invalid", "pass"}, manager.ErrMalformedEntity},
|
||||
{manager.User{"", "pass"}, manager.ErrMalformedEntity},
|
||||
{manager.User{"foo@example.com", ""}, manager.ErrMalformedEntity},
|
||||
"valid user request": {manager.User{"foo@example.com", "pass"}, nil},
|
||||
"malformed e-mail": {manager.User{wrong, "pass"}, manager.ErrMalformedEntity},
|
||||
"empty e-mail": {manager.User{"", "pass"}, manager.ErrMalformedEntity},
|
||||
"empty password": {manager.User{"foo@example.com", ""}, manager.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
req := userReq{tc.user}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{"valid", nil},
|
||||
{"", manager.ErrUnauthorizedAccess},
|
||||
"non-empty token": {uuid.NewV4().String(), nil},
|
||||
"empty token": {"", manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
req := identityReq{tc.key}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddClientReqValidation(t *testing.T) {
|
||||
key := "key"
|
||||
vc := manager.Client{Type: "app"}
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := []struct {
|
||||
key string
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{key, vc, nil},
|
||||
{"", vc, manager.ErrUnauthorizedAccess},
|
||||
{key, manager.Client{Type: "invalid"}, manager.ErrMalformedEntity},
|
||||
"valid client addition request": {client, key, nil},
|
||||
"missing token": {client, "", manager.ErrUnauthorizedAccess},
|
||||
"wrong client type": {manager.Client{Type: wrong}, key, manager.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
req := addClientReq{
|
||||
key: tc.key,
|
||||
client: tc.client,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClientReqValidation(t *testing.T) {
|
||||
key := "key"
|
||||
uuid := gocql.TimeUUID().String()
|
||||
vc := manager.Client{Type: "app"}
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := []struct {
|
||||
key string
|
||||
id string
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{key, uuid, vc, nil},
|
||||
{key, "non-uuid", vc, manager.ErrNotFound},
|
||||
{"", uuid, vc, manager.ErrUnauthorizedAccess},
|
||||
{key, uuid, manager.Client{Type: "invalid"}, manager.ErrMalformedEntity},
|
||||
"valid client update request": {client, id, key, nil},
|
||||
"non-uuid client ID": {client, wrong, key, manager.ErrNotFound},
|
||||
"missing token": {client, id, "", manager.ErrUnauthorizedAccess},
|
||||
"wrong client type": {manager.Client{Type: "invalid"}, id, key, manager.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
req := updateClientReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
@ -93,51 +98,49 @@ func TestUpdateClientReqValidation(t *testing.T) {
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannelReqValidation(t *testing.T) {
|
||||
key := "key"
|
||||
vc := manager.Channel{}
|
||||
key := uuid.NewV4().String()
|
||||
|
||||
cases := []struct {
|
||||
key string
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{key, vc, nil},
|
||||
{"", vc, manager.ErrUnauthorizedAccess},
|
||||
"valid channel creation request": {channel, key, nil},
|
||||
"missing token": {channel, "", manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
req := createChannelReq{
|
||||
key: tc.key,
|
||||
channel: tc.channel,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannelReqValidation(t *testing.T) {
|
||||
key := "key"
|
||||
uuid := gocql.TimeUUID().String()
|
||||
vc := manager.Channel{}
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := []struct {
|
||||
key string
|
||||
id string
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{key, uuid, vc, nil},
|
||||
{key, "non-uuid", vc, manager.ErrNotFound},
|
||||
{"", uuid, vc, manager.ErrUnauthorizedAccess},
|
||||
"valid channel update request": {channel, id, key, nil},
|
||||
"non-uuid channel ID": {channel, wrong, key, manager.ErrNotFound},
|
||||
"missing token": {channel, id, "", manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
req := updateChannelReq{
|
||||
key: tc.key,
|
||||
id: tc.id,
|
||||
@ -145,46 +148,49 @@ func TestUpdateChannelReqValidation(t *testing.T) {
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewResourceReqValidation(t *testing.T) {
|
||||
key := "key"
|
||||
uuid := gocql.TimeUUID().String()
|
||||
key := uuid.NewV4().String()
|
||||
id := uuid.NewV4().String()
|
||||
|
||||
cases := []struct {
|
||||
key string
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{key, uuid, nil},
|
||||
{"", uuid, manager.ErrUnauthorizedAccess},
|
||||
{key, "non-uuid", manager.ErrNotFound},
|
||||
"valid resource viewing request": {id, key, nil},
|
||||
"missing token": {id, "", manager.ErrUnauthorizedAccess},
|
||||
"non-uuid resource ID": {wrong, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
req := viewResourceReq{tc.key, tc.id}
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListResourcesReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
key := uuid.NewV4().String()
|
||||
value := 10
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
size int
|
||||
offset int
|
||||
err error
|
||||
}{
|
||||
{"key", 10, 10, nil},
|
||||
{"", 10, 10, manager.ErrUnauthorizedAccess},
|
||||
{"key", 10, -10, manager.ErrMalformedEntity},
|
||||
{"key", 0, 10, manager.ErrMalformedEntity},
|
||||
{"key", -10, 10, manager.ErrMalformedEntity},
|
||||
"valid listing request": {key, value, value, nil},
|
||||
"missing token": {"", value, value, manager.ErrUnauthorizedAccess},
|
||||
"negative offset": {key, value, -value, manager.ErrMalformedEntity},
|
||||
"zero size": {key, 0, value, manager.ErrMalformedEntity},
|
||||
"negative size": {key, -value, value, manager.ErrMalformedEntity},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
req := listResourcesReq{
|
||||
key: tc.key,
|
||||
size: tc.size,
|
||||
@ -192,6 +198,6 @@ func TestListResourcesReqValidation(t *testing.T) {
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
@ -186,3 +186,31 @@ func (res listChannelsRes) headers() map[string]string {
|
||||
func (res listChannelsRes) empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type connectionRes struct{}
|
||||
|
||||
func (res connectionRes) code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res connectionRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res connectionRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type disconnectionRes struct{}
|
||||
|
||||
func (res disconnectionRes) code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res disconnectionRes) headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res disconnectionRes) empty() bool {
|
||||
return true
|
||||
}
|
||||
|
@ -104,6 +104,20 @@ func MakeHandler(svc manager.Service) http.Handler {
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Put("/channels/:chanId/clients/:clientId", kithttp.NewServer(
|
||||
connectEndpoint(svc),
|
||||
decodeConnection,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Delete("/channels/:chanId/clients/:clientId", kithttp.NewServer(
|
||||
disconnectEndpoint(svc),
|
||||
decodeConnection,
|
||||
encodeResponse,
|
||||
opts...,
|
||||
))
|
||||
|
||||
r.Get("/access-grant", kithttp.NewServer(
|
||||
identityEndpoint(svc),
|
||||
decodeIdentity,
|
||||
@ -218,6 +232,16 @@ func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeConnection(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := connectionReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
chanId: bone.GetValue(r, "chanId"),
|
||||
clientId: bone.GetValue(r, "clientId"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
|
@ -11,8 +11,8 @@ var _ manager.Hasher = (*bcryptHasher)(nil)
|
||||
|
||||
type bcryptHasher struct{}
|
||||
|
||||
// NewHasher instantiates a bcrypt-based hasher implementation.
|
||||
func NewHasher() manager.Hasher {
|
||||
// New instantiates a bcrypt-based hasher implementation.
|
||||
func New() manager.Hasher {
|
||||
return &bcryptHasher{}
|
||||
}
|
||||
|
||||
|
@ -1,115 +0,0 @@
|
||||
package cassandra
|
||||
|
||||
import (
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.ChannelRepository = (*channelRepository)(nil)
|
||||
|
||||
type channelRepository struct {
|
||||
session *gocql.Session
|
||||
}
|
||||
|
||||
// NewChannelRepository instantiates Cassandra channel repository.
|
||||
func NewChannelRepository(session *gocql.Session) manager.ChannelRepository {
|
||||
return &channelRepository{session}
|
||||
}
|
||||
|
||||
func (repo *channelRepository) Save(channel manager.Channel) (string, error) {
|
||||
cql := `INSERT INTO channels_by_user (user, id, name, connected)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
id := gocql.TimeUUID()
|
||||
|
||||
if err := repo.session.Query(cql, channel.Owner, id,
|
||||
channel.Name, channel.Connected).Exec(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return id.String(), nil
|
||||
}
|
||||
|
||||
func (repo *channelRepository) Update(channel manager.Channel) error {
|
||||
cql := `UPDATE channels_by_user SET name = ?, connected = ?
|
||||
WHERE user = ? AND id = ? IF EXISTS`
|
||||
|
||||
if applied, _ := repo.session.Query(cql, channel.Name, channel.Connected,
|
||||
channel.Owner, channel.ID).ScanCAS(); !applied {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *channelRepository) One(owner, id string) (manager.Channel, error) {
|
||||
cql := `SELECT name, connected FROM channels_by_user
|
||||
WHERE user = ? AND id = ? LIMIT 1`
|
||||
|
||||
ch := manager.Channel{
|
||||
Owner: owner,
|
||||
ID: id,
|
||||
}
|
||||
|
||||
if err := repo.session.Query(cql, owner, id).Scan(&ch.Name, &ch.Connected); err != nil {
|
||||
return ch, manager.ErrNotFound
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (repo *channelRepository) All(owner string) []manager.Channel {
|
||||
cql := `SELECT id, name, connected FROM channels_by_user WHERE user = ?`
|
||||
var id string
|
||||
var name string
|
||||
var connected []string
|
||||
|
||||
// NOTE: the closing might failed
|
||||
iter := repo.session.Query(cql, owner).Iter()
|
||||
defer iter.Close()
|
||||
|
||||
channels := make([]manager.Channel, 0)
|
||||
|
||||
for iter.Scan(&id, &name, &connected) {
|
||||
c := manager.Channel{
|
||||
Owner: owner,
|
||||
ID: id,
|
||||
Name: name,
|
||||
Connected: replaceNilWithEmpty(connected),
|
||||
}
|
||||
|
||||
channels = append(channels, c)
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
func replaceNilWithEmpty(items []string) []string {
|
||||
if items != nil {
|
||||
return items
|
||||
}
|
||||
|
||||
return make([]string, 0)
|
||||
}
|
||||
|
||||
func (repo *channelRepository) Remove(owner, id string) error {
|
||||
cql := `DELETE FROM channels_by_user WHERE user = ? AND id = ?`
|
||||
return repo.session.Query(cql, owner, id).Exec()
|
||||
}
|
||||
|
||||
func (repo *channelRepository) HasClient(channel, client string) bool {
|
||||
cql := `SELECT connected FROM clients_by_channel WHERE id = ? LIMIT 1`
|
||||
|
||||
var connected []string
|
||||
|
||||
if err := repo.session.Query(cql, channel).Scan(&connected); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, v := range connected {
|
||||
if v == client {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
package cassandra
|
||||
|
||||
import (
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.ClientRepository = (*clientRepository)(nil)
|
||||
|
||||
type clientRepository struct {
|
||||
session *gocql.Session
|
||||
}
|
||||
|
||||
// NewClientRepository instantiates Cassandra client repository.
|
||||
func NewClientRepository(session *gocql.Session) manager.ClientRepository {
|
||||
return &clientRepository{session}
|
||||
}
|
||||
|
||||
func (repo *clientRepository) Id() string {
|
||||
return gocql.TimeUUID().String()
|
||||
}
|
||||
|
||||
func (repo *clientRepository) Save(client manager.Client) error {
|
||||
cql := `INSERT INTO clients_by_user (user, id, type, name, access_key, meta)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
|
||||
if err := repo.session.Query(cql, client.Owner, client.ID,
|
||||
client.Type, client.Name, client.Key, client.Meta).Exec(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *clientRepository) Update(client manager.Client) error {
|
||||
cql := `UPDATE clients_by_user SET type = ?, name = ?, meta = ?
|
||||
WHERE user = ? AND id = ? IF EXISTS`
|
||||
|
||||
applied, err := repo.session.Query(cql, client.Type, client.Name, client.Meta,
|
||||
client.Owner, client.ID).ScanCAS()
|
||||
|
||||
if !applied {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *clientRepository) One(owner string, id string) (manager.Client, error) {
|
||||
cql := `SELECT type, name, access_key, meta FROM clients_by_user
|
||||
WHERE user = ? AND id = ? LIMIT 1`
|
||||
|
||||
cli := manager.Client{
|
||||
Owner: owner,
|
||||
ID: id,
|
||||
}
|
||||
|
||||
if err := repo.session.Query(cql, owner, id).
|
||||
Scan(&cli.Type, &cli.Name, &cli.Key, &cli.Meta); err != nil {
|
||||
return cli, manager.ErrNotFound
|
||||
}
|
||||
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
func (repo *clientRepository) All(owner string) []manager.Client {
|
||||
cql := `SELECT id, type, name, access_key, meta FROM clients_by_user WHERE user = ?`
|
||||
var id string
|
||||
var cType string
|
||||
var name string
|
||||
var key string
|
||||
var meta map[string]string
|
||||
|
||||
// NOTE: the closing might failed
|
||||
iter := repo.session.Query(cql, owner).Iter()
|
||||
defer iter.Close()
|
||||
|
||||
clients := make([]manager.Client, 0)
|
||||
|
||||
for iter.Scan(&id, &cType, &name, &key, &meta) {
|
||||
c := manager.Client{
|
||||
Owner: owner,
|
||||
ID: id,
|
||||
Type: cType,
|
||||
Name: name,
|
||||
Key: key,
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
clients = append(clients, c)
|
||||
}
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
func (repo *clientRepository) Remove(owner string, id string) error {
|
||||
cql := `DELETE FROM clients_by_user WHERE user = ? AND id = ?`
|
||||
return repo.session.Query(cql, owner, id).Exec()
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
// Package cassandra contains Cassandra-specific repository implementations.
|
||||
package cassandra
|
@ -1,52 +0,0 @@
|
||||
package cassandra
|
||||
|
||||
import "github.com/gocql/gocql"
|
||||
|
||||
var tables []string = []string{
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
email text,
|
||||
password text,
|
||||
PRIMARY KEY (email)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS clients_by_user (
|
||||
user text,
|
||||
id timeuuid,
|
||||
type text,
|
||||
name text,
|
||||
access_key text,
|
||||
meta map<text, text>,
|
||||
PRIMARY KEY ((user), id)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS channels_by_user (
|
||||
user text,
|
||||
id timeuuid,
|
||||
name text,
|
||||
connected set<text>,
|
||||
PRIMARY KEY ((user), id)
|
||||
)`,
|
||||
`CREATE MATERIALIZED VIEW IF NOT EXISTS clients_by_channel
|
||||
AS SELECT user, id, connected FROM channels_by_user
|
||||
WHERE id IS NOT NULL
|
||||
PRIMARY KEY (id, user)
|
||||
`,
|
||||
}
|
||||
|
||||
// Connect establishes connection to the Cassandra cluster.
|
||||
func Connect(hosts []string, keyspace string) (*gocql.Session, error) {
|
||||
cluster := gocql.NewCluster(hosts...)
|
||||
cluster.Keyspace = keyspace
|
||||
cluster.Consistency = gocql.Quorum
|
||||
|
||||
return cluster.CreateSession()
|
||||
}
|
||||
|
||||
// Initialize creates tables used by the service.
|
||||
func Initialize(session *gocql.Session) error {
|
||||
for _, table := range tables {
|
||||
if err := session.Query(table).Exec(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package cassandra
|
||||
|
||||
import (
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.UserRepository = (*userRepository)(nil)
|
||||
|
||||
type userRepository struct {
|
||||
session *gocql.Session
|
||||
}
|
||||
|
||||
// NewUserRepository instantiates Cassandra user repository.
|
||||
func NewUserRepository(session *gocql.Session) manager.UserRepository {
|
||||
return &userRepository{session}
|
||||
}
|
||||
|
||||
func (repo *userRepository) Save(user manager.User) error {
|
||||
cql := `INSERT INTO users (email, password) VALUES (?, ?) IF NOT EXISTS`
|
||||
|
||||
applied, err := repo.session.Query(cql, user.Email, user.Password).ScanCAS()
|
||||
if !applied {
|
||||
return manager.ErrConflict
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *userRepository) One(email string) (manager.User, error) {
|
||||
cql := `SELECT email, password FROM users WHERE email = ? LIMIT 1`
|
||||
|
||||
user := manager.User{}
|
||||
|
||||
if err := repo.session.Query(cql, email).
|
||||
Scan(&user.Email, &user.Password); err != nil {
|
||||
return user, manager.ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
@ -3,10 +3,10 @@ package manager
|
||||
// Channel represents a Mainflux "communication group". This group contains the
|
||||
// clients that can exchange messages between eachother.
|
||||
type Channel struct {
|
||||
Owner string `json:"-"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Connected []string `json:"connected"`
|
||||
ID string `gorm:"type:char(36);primary_key" json:"id"`
|
||||
Owner string `gorm:"type:varchar(254);not null" json:"-"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Clients []Client `gorm:"many2many:channel_clients" json:"connected,omitempty"`
|
||||
}
|
||||
|
||||
// ChannelRepository specifies a channel persistence API.
|
||||
@ -31,6 +31,13 @@ type ChannelRepository interface {
|
||||
// by the specified user.
|
||||
Remove(string, string) error
|
||||
|
||||
// Connect adds client to the channel's list of connected clients.
|
||||
Connect(string, string, string) error
|
||||
|
||||
// Disconnect removes client from the channel's list of connected
|
||||
// clients.
|
||||
Disconnect(string, string, string) error
|
||||
|
||||
// HasClient determines whether the client with the provided identifier, is
|
||||
// "connected" to the specified channel.
|
||||
HasClient(string, string) bool
|
||||
|
@ -5,12 +5,12 @@ import "strings"
|
||||
// Client represents a Mainflux client. Each client is owned by one user, and
|
||||
// it is assigned with the unique identifier and (temporary) access key.
|
||||
type Client struct {
|
||||
Owner string `json:"-"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
ID string `gorm:"type:char(36);primary_key" json:"id"`
|
||||
Owner string `gorm:"type:varchar(254);not null" json:"-"`
|
||||
Type string `gorm:"type:varchar(10);not null" json:"type"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Payload string `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
var clientTypes map[string]bool = map[string]bool{
|
||||
|
@ -18,8 +18,8 @@ type jwtIdentityProvider struct {
|
||||
secret string
|
||||
}
|
||||
|
||||
// NewIdentityProvider instantiates a JWT identity provider.
|
||||
func NewIdentityProvider(secret string) manager.IdentityProvider {
|
||||
// New instantiates a JWT identity provider.
|
||||
func New(secret string) manager.IdentityProvider {
|
||||
return &jwtIdentityProvider{}
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,8 @@ type managerService struct {
|
||||
idp IdentityProvider
|
||||
}
|
||||
|
||||
// NewService instantiates the domain service implementation.
|
||||
func NewService(users UserRepository, clients ClientRepository, channels ChannelRepository,
|
||||
hasher Hasher, idp IdentityProvider) Service {
|
||||
// New instantiates the domain service implementation.
|
||||
func New(users UserRepository, clients ClientRepository, channels ChannelRepository, hasher Hasher, idp IdentityProvider) Service {
|
||||
return &managerService{
|
||||
users: users,
|
||||
clients: clients,
|
||||
@ -183,6 +182,32 @@ func (ms *managerService) RemoveChannel(key, id string) error {
|
||||
return ms.channels.Remove(sub, id)
|
||||
}
|
||||
|
||||
func (ms *managerService) Connect(key, chanId, clientId string) error {
|
||||
owner, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(owner); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Connect(owner, chanId, clientId)
|
||||
}
|
||||
|
||||
func (ms *managerService) Disconnect(key, chanId, clientId string) error {
|
||||
owner, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := ms.users.One(owner); err != nil {
|
||||
return ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ms.channels.Disconnect(owner, chanId, clientId)
|
||||
}
|
||||
|
||||
func (ms *managerService) Identity(key string) (string, error) {
|
||||
client, err := ms.idp.Identity(key)
|
||||
if err != nil {
|
||||
|
@ -9,254 +9,367 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const wrong string = "wrong-value"
|
||||
|
||||
var (
|
||||
users manager.UserRepository = mocks.NewUserRepository()
|
||||
clients manager.ClientRepository = mocks.NewClientRepository()
|
||||
channels manager.ChannelRepository = mocks.NewChannelRepository()
|
||||
hasher manager.Hasher = mocks.NewHasher()
|
||||
idp manager.IdentityProvider = mocks.NewIdentityProvider()
|
||||
svc manager.Service = manager.NewService(users, clients, channels, hasher, idp)
|
||||
user manager.User = manager.User{"user@example.com", "password"}
|
||||
client manager.Client = manager.Client{ID: "1", Type: "app", Name: "test", Key: "1"}
|
||||
channel manager.Channel = manager.Channel{ID: "1", Name: "test", Clients: []manager.Client{client}}
|
||||
)
|
||||
|
||||
func newService() manager.Service {
|
||||
users := mocks.NewUserRepository()
|
||||
clients := mocks.NewClientRepository()
|
||||
channels := mocks.NewChannelRepository()
|
||||
hasher := mocks.NewHasher()
|
||||
idp := mocks.NewIdentityProvider()
|
||||
|
||||
return manager.New(users, clients, channels, hasher, idp)
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
cases := []struct {
|
||||
svc := newService()
|
||||
|
||||
cases := map[string]struct {
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
{manager.User{"foo@bar.com", "pass"}, nil},
|
||||
{manager.User{"foo@bar.com", "pass"}, manager.ErrConflict},
|
||||
"register new user": {user, nil},
|
||||
"register existing user": {user, manager.ErrConflict},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
e := svc.Register(tc.user)
|
||||
assert.Equal(t, tc.err, e, fmt.Sprintf("failed %d\n", i))
|
||||
for desc, tc := range cases {
|
||||
err := svc.Register(tc.user)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
cases := []struct {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
user manager.User
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{manager.User{"foo@bar.com", "pass"}, "foo@bar.com", nil},
|
||||
{manager.User{"new@bar.com", "pass"}, "", manager.ErrUnauthorizedAccess},
|
||||
{manager.User{"foo@bar.com", ""}, "", manager.ErrUnauthorizedAccess},
|
||||
"login with good credentials": {user, nil},
|
||||
"login with wrong e-mail": {manager.User{wrong, user.Password}, manager.ErrUnauthorizedAccess},
|
||||
"login with wrong password": {manager.User{user.Email, wrong}, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
k, e := svc.Login(tc.user)
|
||||
assert.Equal(t, tc.key, k, fmt.Sprintf("bad key at %d\n", i))
|
||||
assert.Equal(t, tc.err, e, fmt.Sprintf("failed %d\n", i))
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.Login(tc.user)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddClient(t *testing.T) {
|
||||
cases := []struct {
|
||||
key string
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{"foo@bar.com", manager.Client{Type: "app", Name: "a"}, "1", nil},
|
||||
{"foo@bar.com", manager.Client{Type: "device", Name: "b"}, "2", nil},
|
||||
{"", manager.Client{Type: "app", Name: "d"}, "", manager.ErrUnauthorizedAccess},
|
||||
"add new app": {manager.Client{Type: "app", Name: "a"}, key, nil},
|
||||
"add new device": {manager.Client{Type: "device", Name: "b"}, key, nil},
|
||||
"add client with wrong credentials": {manager.Client{Type: "app", Name: "d"}, wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
id, err := svc.AddClient(tc.key, tc.client)
|
||||
assert.Equal(t, tc.id, id, fmt.Sprintf("unexpected id at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.AddClient(tc.key, tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateClient(t *testing.T) {
|
||||
cases := []struct {
|
||||
key string
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.AddClient(key, client)
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{"foo@bar.com", manager.Client{ID: "1", Type: "app", Name: "aa"}, nil},
|
||||
{"foo@bar.com", manager.Client{ID: "2", Type: "device", Name: "bb"}, nil},
|
||||
{"", manager.Client{ID: "2", Type: "app", Name: "cc"}, manager.ErrUnauthorizedAccess},
|
||||
{"foo@bar.com", manager.Client{ID: "3", Type: "app", Name: "d"}, manager.ErrNotFound},
|
||||
"update existing client": {client, key, nil},
|
||||
"update client with wrong credentials": {client, wrong, manager.ErrUnauthorizedAccess},
|
||||
"update non-existing client": {manager.Client{ID: "2", Type: "app", Name: "d"}, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
err := svc.UpdateClient(tc.key, tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewClient(t *testing.T) {
|
||||
cases := []struct {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.AddClient(key, client)
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{"1", "foo@bar.com", nil},
|
||||
{"1", "", manager.ErrUnauthorizedAccess},
|
||||
{"5", "foo@bar.com", manager.ErrNotFound},
|
||||
"view existing client": {client.ID, key, nil},
|
||||
"view client with wrong credentials": {client.ID, wrong, manager.ErrUnauthorizedAccess},
|
||||
"view non-existing client": {wrong, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ViewClient(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListClients(t *testing.T) {
|
||||
cases := []struct {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{"foo@bar.com", nil},
|
||||
{"", manager.ErrUnauthorizedAccess},
|
||||
"list clients": {key, nil},
|
||||
"list clients with wrong credentials": {wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ListClients(tc.key)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
cases := []struct {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.AddClient(key, client)
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{"1", "", manager.ErrUnauthorizedAccess},
|
||||
{"1", "foo@bar.com", nil},
|
||||
{"1", "foo@bar.com", nil},
|
||||
{"2", "foo@bar.com", nil},
|
||||
"remove client with wrong credentials": {client.ID, "?", manager.ErrUnauthorizedAccess},
|
||||
"remove existing client": {client.ID, key, nil},
|
||||
"remove removed client": {client.ID, key, nil},
|
||||
"remove non-existing client": {"?", key, nil},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
err := svc.RemoveClient(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateChannel(t *testing.T) {
|
||||
cases := []struct {
|
||||
key string
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{"foo@bar.com", manager.Channel{Connected: []string{"1", "2"}}, "1", nil},
|
||||
{"foo@bar.com", manager.Channel{Connected: []string{"2"}}, "2", nil},
|
||||
{"", manager.Channel{Connected: []string{"1"}}, "", manager.ErrUnauthorizedAccess},
|
||||
"create channel": {manager.Channel{}, key, nil},
|
||||
"create channel with wrong credentials": {manager.Channel{}, wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
id, err := svc.CreateChannel(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.id, id, fmt.Sprintf("unexpected id at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.CreateChannel(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
cases := []struct {
|
||||
key string
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.CreateChannel(key, channel)
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{"foo@bar.com", manager.Channel{ID: "1", Connected: []string{"1"}}, nil},
|
||||
{"foo@bar.com", manager.Channel{ID: "2", Connected: []string{}}, nil},
|
||||
{"", manager.Channel{ID: "2", Connected: []string{"1"}}, manager.ErrUnauthorizedAccess},
|
||||
{"foo@bar.com", manager.Channel{ID: "3", Connected: []string{"1"}}, manager.ErrNotFound},
|
||||
"update existing channel": {channel, key, nil},
|
||||
"update channel with wrong credentials": {channel, wrong, manager.ErrUnauthorizedAccess},
|
||||
"update non-existing channel": {manager.Channel{ID: "2", Name: "test"}, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
err := svc.UpdateChannel(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewChannel(t *testing.T) {
|
||||
cases := []struct {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.CreateChannel(key, channel)
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{"1", "foo@bar.com", nil},
|
||||
{"1", "", manager.ErrUnauthorizedAccess},
|
||||
{"5", "foo@bar.com", manager.ErrNotFound},
|
||||
"view existing channel": {channel.ID, key, nil},
|
||||
"view channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
|
||||
"view non-existing channel": {wrong, key, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ViewChannel(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListChannels(t *testing.T) {
|
||||
cases := []struct {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{"foo@bar.com", nil},
|
||||
{"", manager.ErrUnauthorizedAccess},
|
||||
"list channels": {key, nil},
|
||||
"list channels with wrong credentials": {wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.ListChannels(tc.key)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentity(t *testing.T) {
|
||||
cases := []struct {
|
||||
key string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{"foo@bar.com", "foo@bar.com", nil},
|
||||
{"", "", manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
id, err := svc.Identity(tc.key)
|
||||
assert.Equal(t, tc.id, id, fmt.Sprintf("unexpected id at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanAccess(t *testing.T) {
|
||||
cases := []struct {
|
||||
key string
|
||||
channel string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{"1", "1", "1", nil},
|
||||
{"1", "2", "", manager.ErrUnauthorizedAccess},
|
||||
{"", "1", "", manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
id, err := svc.CanAccess(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.id, id, fmt.Sprintf("unexpected id at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
cases := []struct {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
svc.CreateChannel(key, channel)
|
||||
|
||||
cases := map[string]struct {
|
||||
id string
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
{"1", "", manager.ErrUnauthorizedAccess},
|
||||
{"1", "foo@bar.com", nil},
|
||||
{"1", "foo@bar.com", nil},
|
||||
{"2", "foo@bar.com", nil},
|
||||
"remove channel with wrong credentials": {channel.ID, wrong, manager.ErrUnauthorizedAccess},
|
||||
"remove existing channel": {channel.ID, key, nil},
|
||||
"remove removed channel": {channel.ID, key, nil},
|
||||
"remove non-existing channel": {channel.ID, key, nil},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
for desc, tc := range cases {
|
||||
err := svc.RemoveChannel(tc.key, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("failed at %d\n", i))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
chanId string
|
||||
clientId string
|
||||
err error
|
||||
}{
|
||||
"connect client": {key, chanId, clientId, nil},
|
||||
"connect client with wrong credentials": {wrong, chanId, clientId, manager.ErrUnauthorizedAccess},
|
||||
"connect client to non-existing channel": {key, wrong, clientId, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.Connect(tc.key, tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnect(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
clientId, _ := svc.AddClient(key, client)
|
||||
chanId, _ := svc.CreateChannel(key, channel)
|
||||
|
||||
svc.Connect(key, chanId, clientId)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
chanId string
|
||||
clientId string
|
||||
err error
|
||||
}{
|
||||
"disconnect connected client": {key, chanId, clientId, nil},
|
||||
"disconnect disconnected client": {key, chanId, clientId, manager.ErrNotFound},
|
||||
"disconnect client with wrong credentials": {wrong, chanId, clientId, manager.ErrUnauthorizedAccess},
|
||||
"disconnect client from non-existing channel": {key, wrong, clientId, manager.ErrNotFound},
|
||||
"disconnect non-existing client": {key, chanId, wrong, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := svc.Disconnect(tc.key, tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIdentity(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
err error
|
||||
}{
|
||||
"valid token's identity": {key, nil},
|
||||
"invalid token's identity": {"", manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.Identity(tc.key)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanAccess(t *testing.T) {
|
||||
svc := newService()
|
||||
svc.Register(user)
|
||||
key, _ := svc.Login(user)
|
||||
|
||||
svc.AddClient(key, client)
|
||||
svc.CreateChannel(key, channel)
|
||||
|
||||
cases := map[string]struct {
|
||||
key string
|
||||
channel string
|
||||
err error
|
||||
}{
|
||||
"allowed access": {client.Key, channel.ID, nil},
|
||||
"not-connected cannot access": {wrong, channel.ID, manager.ErrUnauthorizedAccess},
|
||||
"access non-existing channel": {client.Key, wrong, manager.ErrUnauthorizedAccess},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := svc.CanAccess(tc.key, tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
@ -24,46 +24,48 @@ func NewChannelRepository() manager.ChannelRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *channelRepositoryMock) Save(channel manager.Channel) (string, error) {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
func (crm *channelRepositoryMock) Save(channel manager.Channel) (string, error) {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
repo.counter += 1
|
||||
channel.ID = strconv.Itoa(repo.counter)
|
||||
crm.counter += 1
|
||||
channel.ID = strconv.Itoa(crm.counter)
|
||||
|
||||
repo.channels[key(channel.Owner, channel.ID)] = channel
|
||||
crm.channels[key(channel.Owner, channel.ID)] = channel
|
||||
|
||||
return channel.ID, nil
|
||||
}
|
||||
|
||||
func (repo *channelRepositoryMock) Update(channel manager.Channel) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
func (crm *channelRepositoryMock) Update(channel manager.Channel) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
dbKey := key(channel.Owner, channel.ID)
|
||||
|
||||
if _, ok := repo.channels[dbKey]; !ok {
|
||||
if _, ok := crm.channels[dbKey]; !ok {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
repo.channels[dbKey] = channel
|
||||
crm.channels[dbKey] = channel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *channelRepositoryMock) One(owner, id string) (manager.Channel, error) {
|
||||
if c, ok := repo.channels[key(owner, id)]; ok {
|
||||
func (crm *channelRepositoryMock) One(owner, id string) (manager.Channel, error) {
|
||||
if c, ok := crm.channels[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return manager.Channel{}, manager.ErrNotFound
|
||||
}
|
||||
|
||||
func (repo *channelRepositoryMock) All(owner string) []manager.Channel {
|
||||
func (crm *channelRepositoryMock) All(owner string) []manager.Channel {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
prefix := fmt.Sprintf("%s-", owner)
|
||||
|
||||
channels := make([]manager.Channel, 0)
|
||||
|
||||
for k, v := range repo.channels {
|
||||
for k, v := range crm.channels {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
channels = append(channels, v)
|
||||
}
|
||||
@ -72,21 +74,58 @@ func (repo *channelRepositoryMock) All(owner string) []manager.Channel {
|
||||
return channels
|
||||
}
|
||||
|
||||
func (repo *channelRepositoryMock) Remove(owner, id string) error {
|
||||
delete(repo.channels, key(owner, id))
|
||||
func (crm *channelRepositoryMock) Remove(owner, id string) error {
|
||||
delete(crm.channels, key(owner, id))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *channelRepositoryMock) HasClient(channel, client string) bool {
|
||||
func (crm *channelRepositoryMock) Connect(owner, chanId, clientId string) error {
|
||||
channel, err := crm.One(owner, chanId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Since the current implementation has no way to retrieve a real client
|
||||
// instance, the implementation will assume client always exist and create
|
||||
// a dummy one, containing only the provided ID.
|
||||
channel.Clients = append(channel.Clients, manager.Client{ID: clientId})
|
||||
return crm.Update(channel)
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) Disconnect(owner, chanId, clientId string) error {
|
||||
channel, err := crm.One(owner, chanId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !crm.HasClient(chanId, clientId) {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
connected := make([]manager.Client, len(channel.Clients)-1)
|
||||
for _, client := range channel.Clients {
|
||||
if client.ID != clientId {
|
||||
connected = append(connected, client)
|
||||
}
|
||||
}
|
||||
|
||||
channel.Clients = connected
|
||||
return crm.Update(channel)
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) HasClient(channel, client string) bool {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
suffix := fmt.Sprintf("-%s", channel)
|
||||
|
||||
for k, v := range repo.channels {
|
||||
for k, v := range crm.channels {
|
||||
if strings.HasSuffix(k, suffix) {
|
||||
for _, c := range v.Connected {
|
||||
if c == client {
|
||||
for _, c := range v.Clients {
|
||||
if c.ID == client {
|
||||
return true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,52 +24,54 @@ func NewClientRepository() manager.ClientRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *clientRepositoryMock) Id() string {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
func (crm *clientRepositoryMock) Id() string {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
repo.counter += 1
|
||||
return strconv.Itoa(repo.counter)
|
||||
crm.counter += 1
|
||||
return strconv.Itoa(crm.counter)
|
||||
}
|
||||
|
||||
func (repo *clientRepositoryMock) Save(client manager.Client) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
func (crm *clientRepositoryMock) Save(client manager.Client) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
repo.clients[key(client.Owner, client.ID)] = client
|
||||
crm.clients[key(client.Owner, client.ID)] = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *clientRepositoryMock) Update(client manager.Client) error {
|
||||
repo.mu.Lock()
|
||||
defer repo.mu.Unlock()
|
||||
func (crm *clientRepositoryMock) Update(client manager.Client) error {
|
||||
crm.mu.Lock()
|
||||
defer crm.mu.Unlock()
|
||||
|
||||
dbKey := key(client.Owner, client.ID)
|
||||
|
||||
if _, ok := repo.clients[dbKey]; !ok {
|
||||
if _, ok := crm.clients[dbKey]; !ok {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
repo.clients[dbKey] = client
|
||||
crm.clients[dbKey] = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *clientRepositoryMock) One(owner, id string) (manager.Client, error) {
|
||||
if c, ok := repo.clients[key(owner, id)]; ok {
|
||||
func (crm *clientRepositoryMock) One(owner, id string) (manager.Client, error) {
|
||||
if c, ok := crm.clients[key(owner, id)]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
return manager.Client{}, manager.ErrNotFound
|
||||
}
|
||||
|
||||
func (repo *clientRepositoryMock) All(owner string) []manager.Client {
|
||||
func (crm *clientRepositoryMock) All(owner string) []manager.Client {
|
||||
// This obscure way to examine map keys is enforced by the key structure
|
||||
// itself (see mocks/commons.go).
|
||||
prefix := fmt.Sprintf("%s-", owner)
|
||||
|
||||
clients := make([]manager.Client, 0)
|
||||
|
||||
for k, v := range repo.clients {
|
||||
for k, v := range crm.clients {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
clients = append(clients, v)
|
||||
}
|
||||
@ -78,7 +80,7 @@ func (repo *clientRepositoryMock) All(owner string) []manager.Client {
|
||||
return clients
|
||||
}
|
||||
|
||||
func (repo *clientRepositoryMock) Remove(owner, id string) error {
|
||||
delete(repo.clients, key(owner, id))
|
||||
func (crm *clientRepositoryMock) Remove(owner, id string) error {
|
||||
delete(crm.clients, key(owner, id))
|
||||
return nil
|
||||
}
|
||||
|
@ -2,6 +2,10 @@ package mocks
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Since mocks will store data in map, and they need to resemble the real
|
||||
// identifiers as much as possible, a key will be created as combination of
|
||||
// owner and their own identifiers. This will allow searching either by
|
||||
// prefix or suffix.
|
||||
func key(owner, id string) string {
|
||||
return fmt.Sprintf("%s-%s", owner, id)
|
||||
}
|
||||
|
@ -20,23 +20,23 @@ func NewUserRepository() manager.UserRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (ur *userRepositoryMock) Save(user manager.User) error {
|
||||
ur.mu.Lock()
|
||||
defer ur.mu.Unlock()
|
||||
func (urm *userRepositoryMock) Save(user manager.User) error {
|
||||
urm.mu.Lock()
|
||||
defer urm.mu.Unlock()
|
||||
|
||||
if _, ok := ur.users[user.Email]; ok {
|
||||
if _, ok := urm.users[user.Email]; ok {
|
||||
return manager.ErrConflict
|
||||
}
|
||||
|
||||
ur.users[user.Email] = user
|
||||
urm.users[user.Email] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ur *userRepositoryMock) One(email string) (manager.User, error) {
|
||||
ur.mu.Lock()
|
||||
defer ur.mu.Unlock()
|
||||
func (urm *userRepositoryMock) One(email string) (manager.User, error) {
|
||||
urm.mu.Lock()
|
||||
defer urm.mu.Unlock()
|
||||
|
||||
if val, ok := ur.users[email]; ok {
|
||||
if val, ok := urm.users[email]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
|
119
manager/postgres/channels.go
Normal file
119
manager/postgres/channels.go
Normal file
@ -0,0 +1,119 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var _ manager.ChannelRepository = (*channelRepository)(nil)
|
||||
|
||||
type channelRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewChannelRepository(db *gorm.DB) manager.ChannelRepository {
|
||||
return &channelRepository{db}
|
||||
}
|
||||
|
||||
func (cr channelRepository) Save(channel manager.Channel) (string, error) {
|
||||
channel.ID = uuid.NewV4().String()
|
||||
|
||||
if err := cr.db.Create(&channel).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return channel.ID, nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Update(channel manager.Channel) error {
|
||||
sql := "UPDATE channels SET name = ? WHERE owner = ? AND id = ?;"
|
||||
res := cr.db.Exec(sql, channel.Name, channel.Owner, channel.ID)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr channelRepository) One(owner, id string) (manager.Channel, error) {
|
||||
channel := manager.Channel{}
|
||||
|
||||
res := cr.db.Preload("Clients").First(&channel, "owner = ? AND id = ?", owner, id)
|
||||
|
||||
if err := res.Error; err != nil {
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
return channel, manager.ErrNotFound
|
||||
}
|
||||
|
||||
return channel, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) All(owner string) []manager.Channel {
|
||||
var channels []manager.Channel
|
||||
|
||||
cr.db.Find(&channels, "owner = ?", owner)
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
func (cr channelRepository) Remove(owner, id string) error {
|
||||
cr.db.Delete(&manager.Channel{}, "owner = ? AND id = ?", owner, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr channelRepository) Connect(owner, chanId, clientId string) error {
|
||||
// This approach can be replaced by declaring composite keys on both tables
|
||||
// (clients and channels), and then propagate them into the m2m table. For
|
||||
// some reason GORM does not infer these kind of connections well and
|
||||
// raises a "no unique constraint for referenced table". Until we find a
|
||||
// way to properly represent this relationship, let's stick with the nested
|
||||
// query approach and observe its behaviour.
|
||||
sql := `INSERT INTO channel_clients (channel_id, client_id)
|
||||
SELECT ?, ? WHERE
|
||||
EXISTS (SELECT 1 FROM channels WHERE owner = ? AND id = ?) AND
|
||||
EXISTS (SELECT 1 FROM clients WHERE owner = ? AND id = ?);`
|
||||
|
||||
res := cr.db.Exec(sql, chanId, clientId, owner, chanId, owner, clientId)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr channelRepository) Disconnect(owner, chanId, clientId string) error {
|
||||
// The same remark given in Connect applies here.
|
||||
sql := `DELETE FROM channel_clients WHERE
|
||||
channel_id = ? AND client_id = ? AND
|
||||
EXISTS (SELECT 1 FROM channels WHERE owner = ? AND id = ?) AND
|
||||
EXISTS (SELECT 1 FROM clients WHERE owner = ? AND id = ?);`
|
||||
|
||||
res := cr.db.Exec(sql, chanId, clientId, owner, chanId, owner, clientId)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr channelRepository) HasClient(chanId, clientId string) bool {
|
||||
sql := "SELECT EXISTS (SELECT 1 FROM channel_clients WHERE channel_id = $1 AND client_id = $2);"
|
||||
|
||||
row := cr.db.DB().QueryRow(sql, chanId, clientId)
|
||||
|
||||
var exists bool
|
||||
if err := row.Scan(&exists); err != nil {
|
||||
// TODO: this error should be logged
|
||||
return false
|
||||
}
|
||||
|
||||
return exists
|
||||
}
|
246
manager/postgres/channels_test.go
Normal file
246
manager/postgres/channels_test.go
Normal file
@ -0,0 +1,246 @@
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChannelSave(t *testing.T) {
|
||||
email := "channel-save@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
c1 := manager.Channel{Owner: email}
|
||||
c2 := manager.Channel{Owner: wrong}
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
hasErr bool
|
||||
}{
|
||||
"new channel, existing user": {c1, false},
|
||||
"new channel, non-existing user": {c2, true},
|
||||
}
|
||||
|
||||
channelRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := channelRepo.Save(tc.channel)
|
||||
hasErr := err != nil
|
||||
assert.Equal(t, tc.hasErr, hasErr, fmt.Sprintf("%s: expected %t got %t", desc, tc.hasErr, hasErr))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelUpdate(t *testing.T) {
|
||||
email := "channel-update@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
c := manager.Channel{Owner: email}
|
||||
id, _ := chanRepo.Save(c)
|
||||
c.ID = id
|
||||
|
||||
cases := map[string]struct {
|
||||
channel manager.Channel
|
||||
err error
|
||||
}{
|
||||
"existing channel": {c, nil},
|
||||
"non-existing channel with existing user": {manager.Channel{ID: wrong, Owner: email}, manager.ErrNotFound},
|
||||
"non-existing channel with non-existing user": {manager.Channel{ID: wrong, Owner: wrong}, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := chanRepo.Update(tc.channel)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleChannelRetrieval(t *testing.T) {
|
||||
email := "channel-single-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
c := manager.Channel{Owner: email}
|
||||
id, _ := chanRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
ID string
|
||||
err error
|
||||
}{
|
||||
"existing user": {c.Owner, id, nil},
|
||||
"existing user, non-existing channel": {c.Owner, wrong, manager.ErrNotFound},
|
||||
"non-existing owner": {wrong, id, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := chanRepo.One(tc.owner, tc.ID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiChannelRetrieval(t *testing.T) {
|
||||
email := "channel-multi-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
|
||||
n := 10
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
c := manager.Channel{Owner: email}
|
||||
chanRepo.Save(c)
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
len int
|
||||
}{
|
||||
"existing owner": {email, n},
|
||||
"non-existing owner": {wrong, 0},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
n := len(chanRepo.All(tc.owner))
|
||||
assert.Equal(t, tc.len, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.len, n))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelRemoval(t *testing.T) {
|
||||
email := "channel-removal@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
|
||||
// show that the removal works the same for both existing and non-existing
|
||||
// (removed) channel
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := chanRepo.Remove(email, chanId); err != nil {
|
||||
t.Fatalf("#%d: failed to remove channel due to: %s", i, err)
|
||||
}
|
||||
|
||||
if _, err := chanRepo.One(email, chanId); err != manager.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, manager.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelConnect(t *testing.T) {
|
||||
email := "channel-connect@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
chanId string
|
||||
clientId string
|
||||
err error
|
||||
}{
|
||||
"existing user, channel and client": {email, chanId, client.ID, nil},
|
||||
"with non-existing user": {wrong, chanId, client.ID, manager.ErrNotFound},
|
||||
"non-existing channel": {email, wrong, client.ID, manager.ErrNotFound},
|
||||
"non-existing client": {email, chanId, wrong, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := chanRepo.Connect(tc.owner, tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDisconnect(t *testing.T) {
|
||||
email := "channel-disconnect@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
|
||||
chanRepo.Connect(email, chanId, client.ID)
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
chanId string
|
||||
clientId string
|
||||
err error
|
||||
}{
|
||||
"connected client": {email, chanId, client.ID, nil},
|
||||
"non-connected client": {email, chanId, client.ID, manager.ErrNotFound},
|
||||
"non-existing user": {wrong, chanId, client.ID, manager.ErrNotFound},
|
||||
"non-existing channel": {email, wrong, client.ID, manager.ErrNotFound},
|
||||
"non-existing client": {email, chanId, wrong, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := chanRepo.Disconnect(tc.owner, tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelAccessCheck(t *testing.T) {
|
||||
email := "channel-access-check@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
chanId, _ := chanRepo.Save(manager.Channel{Owner: email})
|
||||
|
||||
chanRepo.Connect(email, chanId, client.ID)
|
||||
|
||||
cases := map[string]struct {
|
||||
chanId string
|
||||
clientId string
|
||||
hasAccess bool
|
||||
}{
|
||||
"client that has access": {chanId, client.ID, true},
|
||||
"client without access": {chanId, wrong, false},
|
||||
"check access to non-existing channel": {wrong, client.ID, false},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
hasAccess := chanRepo.HasClient(tc.chanId, tc.clientId)
|
||||
assert.Equal(t, tc.hasAccess, hasAccess, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasAccess, hasAccess))
|
||||
}
|
||||
}
|
72
manager/postgres/clients.go
Normal file
72
manager/postgres/clients.go
Normal file
@ -0,0 +1,72 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var _ manager.ClientRepository = (*clientRepository)(nil)
|
||||
|
||||
type clientRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewClientRepository instantiates a PostgreSQL implementation of client
|
||||
// repository.
|
||||
func NewClientRepository(db *gorm.DB) manager.ClientRepository {
|
||||
return &clientRepository{db}
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Id() string {
|
||||
return uuid.NewV4().String()
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Save(client manager.Client) error {
|
||||
if err := cr.db.Create(&client).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Update(client manager.Client) error {
|
||||
sql := "UPDATE clients SET name = ?, payload = ? WHERE owner = ? AND id = ?;"
|
||||
res := cr.db.Exec(sql, client.Name, client.Payload, client.Owner, client.ID)
|
||||
|
||||
if res.Error == nil && res.RowsAffected == 0 {
|
||||
return manager.ErrNotFound
|
||||
}
|
||||
|
||||
return res.Error
|
||||
}
|
||||
|
||||
func (cr *clientRepository) One(owner, id string) (manager.Client, error) {
|
||||
client := manager.Client{}
|
||||
|
||||
res := cr.db.First(&client, "owner = ? AND id = ?", owner, id)
|
||||
|
||||
if err := res.Error; err != nil {
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
return client, manager.ErrNotFound
|
||||
}
|
||||
|
||||
return client, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (cr *clientRepository) All(owner string) []manager.Client {
|
||||
var clients []manager.Client
|
||||
|
||||
cr.db.Find(&clients, "owner = ?", owner)
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
func (cr *clientRepository) Remove(owner, id string) error {
|
||||
cr.db.Delete(&manager.Client{}, "owner = ? AND id = ?", owner, id)
|
||||
return nil
|
||||
}
|
162
manager/postgres/clients_test.go
Normal file
162
manager/postgres/clients_test.go
Normal file
@ -0,0 +1,162 @@
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClientSave(t *testing.T) {
|
||||
email := "client-save@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
c1 := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
c2 := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: "unknown@example.com",
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
hasErr bool
|
||||
}{
|
||||
"new client, existing user": {c1, false},
|
||||
"new client, non-existing user": {c2, true},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
hasErr := clientRepo.Save(tc.client) != nil
|
||||
assert.Equal(t, tc.hasErr, hasErr, fmt.Sprintf("%s: expected %t got %t\n", desc, tc.hasErr, hasErr))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientUpdate(t *testing.T) {
|
||||
email := "client-update@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
c := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
client manager.Client
|
||||
err error
|
||||
}{
|
||||
"existing client": {c, nil},
|
||||
"non-existing client with existing user": {manager.Client{ID: wrong, Owner: email}, manager.ErrNotFound},
|
||||
"non-existing client with non-existing user": {manager.Client{ID: wrong, Owner: wrong}, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := clientRepo.Update(tc.client)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleClientRetrieval(t *testing.T) {
|
||||
email := "client-single-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
c := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
ID string
|
||||
err error
|
||||
}{
|
||||
"existing user": {c.Owner, c.ID, nil},
|
||||
"existing user, non-existing client": {c.Owner, wrong, manager.ErrNotFound},
|
||||
"non-existing owner": {wrong, c.ID, manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := clientRepo.One(tc.owner, tc.ID)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiClientRetrieval(t *testing.T) {
|
||||
email := "client-multi-retrieval@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
|
||||
n := 10
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
c := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
clientRepo.Save(c)
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
owner string
|
||||
len int
|
||||
}{
|
||||
"existing owner": {email, n},
|
||||
"non-existing owner": {wrong, 0},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
n := len(clientRepo.All(tc.owner))
|
||||
assert.Equal(t, tc.len, n, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.len, n))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRemoval(t *testing.T) {
|
||||
email := "client-removal@example.com"
|
||||
|
||||
userRepo := postgres.NewUserRepository(db)
|
||||
userRepo.Save(manager.User{email, "pass"})
|
||||
|
||||
clientRepo := postgres.NewClientRepository(db)
|
||||
client := manager.Client{
|
||||
ID: clientRepo.Id(),
|
||||
Owner: email,
|
||||
}
|
||||
clientRepo.Save(client)
|
||||
|
||||
// show that the removal works the same for both existing and non-existing
|
||||
// (removed) client
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := clientRepo.Remove(email, client.ID); err != nil {
|
||||
t.Fatalf("#%d: failed to remove client due to: %s", i, err)
|
||||
}
|
||||
|
||||
if _, err := clientRepo.One(email, client.ID); err != manager.ErrNotFound {
|
||||
t.Fatalf("#%d: expected %s got %s", i, manager.ErrNotFound, err)
|
||||
}
|
||||
}
|
||||
}
|
3
manager/postgres/doc.go
Normal file
3
manager/postgres/doc.go
Normal file
@ -0,0 +1,3 @@
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
48
manager/postgres/init.go
Normal file
48
manager/postgres/init.go
Normal file
@ -0,0 +1,48 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
const errDuplicate string = "unique_violation"
|
||||
|
||||
type connection struct {
|
||||
ClientID string `gorm:"primary_key"`
|
||||
ChannelID string `gorm:"primary_key"`
|
||||
}
|
||||
|
||||
func (c connection) TableName() string {
|
||||
return "channel_clients"
|
||||
}
|
||||
|
||||
// Connect creates a connection to the PostgreSQL instance. A non-nil error
|
||||
// is returned to indicate failure.
|
||||
func Connect(host, port, name, user, pass string) (*gorm.DB, error) {
|
||||
t := "host=%s port=%s user=%s dbname=%s password=%s sslmode=disable"
|
||||
url := fmt.Sprintf(t, host, port, user, name, pass)
|
||||
|
||||
db, err := gorm.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db = db.AutoMigrate(&manager.User{}, &manager.Client{}, &manager.Channel{}, &connection{})
|
||||
|
||||
db = db.Model(&manager.Client{}).
|
||||
AddForeignKey("owner", "users(email)", "CASCADE", "CASCADE").
|
||||
AddUniqueIndex("idx_pk_client", "id", "owner")
|
||||
|
||||
db = db.Model(&manager.Channel{}).
|
||||
AddForeignKey("owner", "users(email)", "CASCADE", "CASCADE").
|
||||
AddUniqueIndex("idx_pk_channel", "id", "owner")
|
||||
|
||||
db = db.Model(&connection{}).
|
||||
AddForeignKey("client_id", "clients(id)", "CASCADE", "CASCADE").
|
||||
AddForeignKey("channel_id", "channels(id)", "CASCADE", "CASCADE")
|
||||
|
||||
return db.LogMode(false), nil
|
||||
}
|
62
manager/postgres/setup_test.go
Normal file
62
manager/postgres/setup_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Package postgres_test contains tests for PostgreSQL repository
|
||||
// implementations.
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"gopkg.in/ory-am/dockertest.v3"
|
||||
)
|
||||
|
||||
const wrong string = "wrong-value"
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
cfg := []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
}
|
||||
container, err := pool.Run("postgres", "10.2-alpine", cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
if db, err = postgres.Connect("localhost", port, "test", "test", "test"); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
48
manager/postgres/users.go
Normal file
48
manager/postgres/users.go
Normal file
@ -0,0 +1,48 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
"github.com/lib/pq"
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
)
|
||||
|
||||
var _ manager.UserRepository = (*userRepository)(nil)
|
||||
|
||||
type userRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserRepository instantiates a PostgreSQL implementation of user
|
||||
// repository.
|
||||
func NewUserRepository(db *gorm.DB) manager.UserRepository {
|
||||
return &userRepository{db}
|
||||
}
|
||||
|
||||
func (ur *userRepository) Save(user manager.User) error {
|
||||
if err := ur.db.Create(&user).Error; err != nil {
|
||||
if pqErr, ok := err.(*pq.Error); ok && errDuplicate == pqErr.Code.Name() {
|
||||
return manager.ErrConflict
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ur *userRepository) One(email string) (manager.User, error) {
|
||||
user := manager.User{}
|
||||
|
||||
q := ur.db.First(&user, "email = ?", email)
|
||||
|
||||
if err := q.Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return user, manager.ErrNotFound
|
||||
}
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
49
manager/postgres/users_test.go
Normal file
49
manager/postgres/users_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mainflux/mainflux/manager"
|
||||
"github.com/mainflux/mainflux/manager/postgres"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserSave(t *testing.T) {
|
||||
email := "user-save@example.com"
|
||||
|
||||
cases := map[string]struct {
|
||||
user manager.User
|
||||
err error
|
||||
}{
|
||||
"new user": {manager.User{email, "pass"}, nil},
|
||||
"duplicate user": {manager.User{email, "pass"}, manager.ErrConflict},
|
||||
}
|
||||
|
||||
repo := postgres.NewUserRepository(db)
|
||||
|
||||
for desc, tc := range cases {
|
||||
err := repo.Save(tc.user)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleUserRetrieval(t *testing.T) {
|
||||
email := "user-retrieval@example.com"
|
||||
|
||||
repo := postgres.NewUserRepository(db)
|
||||
repo.Save(manager.User{email, "pass"})
|
||||
|
||||
cases := map[string]struct {
|
||||
email string
|
||||
err error
|
||||
}{
|
||||
"existing user": {email, nil},
|
||||
"non-existing user": {"unknown@example.com", manager.ErrNotFound},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
_, err := repo.One(tc.email)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
}
|
||||
}
|
@ -69,6 +69,13 @@ type Service interface {
|
||||
// belongs to the user identified by the provided key.
|
||||
RemoveChannel(string, string) error
|
||||
|
||||
// Connect adds client to the channel's list of connected clients.
|
||||
Connect(string, string, string) error
|
||||
|
||||
// Disconnect removes client from the channel's list of connected
|
||||
// clients.
|
||||
Disconnect(string, string, string) error
|
||||
|
||||
// Identity retrieves Client ID for provided client token.
|
||||
Identity(string) (string, error)
|
||||
|
||||
|
@ -115,14 +115,14 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/clients/{id}:
|
||||
/clients/{clientId}:
|
||||
get:
|
||||
summary: Retrieves client info
|
||||
tags:
|
||||
- clients
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Id"
|
||||
- $ref: "#/parameters/ClientId"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
@ -138,13 +138,13 @@ paths:
|
||||
summary: Updates client info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Resource's unique identifier will not be
|
||||
affected. Note that the client's type and ID cannot be changed.
|
||||
provided in a request payload. Note that the client's type and ID
|
||||
cannot be changed.
|
||||
tags:
|
||||
- clients
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Id"
|
||||
- $ref: "#/parameters/ClientId"
|
||||
- name: client
|
||||
description: JSON-formatted document describing the updated client.
|
||||
in: body
|
||||
@ -171,7 +171,7 @@ paths:
|
||||
- clients
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Id"
|
||||
- $ref: "#/parameters/ClientId"
|
||||
responses:
|
||||
204:
|
||||
description: Client removed.
|
||||
@ -238,14 +238,14 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/channels/{id}:
|
||||
/channels/{chanId}:
|
||||
get:
|
||||
summary: Retrieves channel info
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Id"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
@ -261,13 +261,13 @@ paths:
|
||||
summary: Updates channel info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Resource's unique identifier will not be
|
||||
provided in a request payload. Note that the channel's ID will not be
|
||||
affected.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Id"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
- name: channel
|
||||
description: JSON-formatted document describing the updated channel.
|
||||
in: body
|
||||
@ -294,7 +294,7 @@ paths:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Id"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
responses:
|
||||
204:
|
||||
description: Channel removed.
|
||||
@ -302,6 +302,47 @@ paths:
|
||||
description: Missing or invalid access token provided.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/channels/{chanId}/clients/{clientId}:
|
||||
put:
|
||||
summary: Connects the client to the channel
|
||||
description: |
|
||||
Creates connection between a client and a channel. Once connected to
|
||||
the channel, clients are allowed to exchange messages through it.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
- $ref: "#/parameters/ClientId"
|
||||
responses:
|
||||
200:
|
||||
description: Client connected.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Channel or client does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
delete:
|
||||
summary: Disconnects the client from the channel
|
||||
description: |
|
||||
Removes connection between a client and a channel. Once connection is
|
||||
removed, client can no longer exchange messages through the channel.
|
||||
tags:
|
||||
- channels
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
- $ref: "#/parameters/ClientId"
|
||||
responses:
|
||||
204:
|
||||
description: Client disconnected.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
404:
|
||||
description: Channel or client does not exist.
|
||||
500:
|
||||
$ref: "#/responses/ServiceError"
|
||||
/access-grant:
|
||||
get:
|
||||
summary: Checks the token validity
|
||||
@ -322,7 +363,7 @@ paths:
|
||||
description: ID of the entity bound to the provided access key.
|
||||
403:
|
||||
description: Missing or invalid access token provided.
|
||||
/channels/{id}/access-grant:
|
||||
/channels/{chanId}/access-grant:
|
||||
get:
|
||||
summary: Checks channel accessibility
|
||||
description: |
|
||||
@ -332,7 +373,7 @@ paths:
|
||||
- access control
|
||||
parameters:
|
||||
- $ref: "#/parameters/Authorization"
|
||||
- $ref: "#/parameters/Id"
|
||||
- $ref: "#/parameters/ChanId"
|
||||
responses:
|
||||
200:
|
||||
description: Client can access the channel.
|
||||
@ -352,9 +393,16 @@ parameters:
|
||||
in: header
|
||||
type: string
|
||||
required: true
|
||||
Id:
|
||||
name: id
|
||||
description: Unique resource identifier.
|
||||
ChanId:
|
||||
name: chanId
|
||||
description: Unique channel identifier.
|
||||
in: path
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
ClientId:
|
||||
name: clientId
|
||||
description: Unique client identifier.
|
||||
in: path
|
||||
type: string
|
||||
format: uuid
|
||||
@ -387,7 +435,16 @@ definitions:
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/definitions/ChannelRes"
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique channel identifier generated by the service.
|
||||
name:
|
||||
type: string
|
||||
description: Free-form channel name.
|
||||
required:
|
||||
- id
|
||||
required:
|
||||
- channels
|
||||
ChannelRes:
|
||||
@ -404,23 +461,15 @@ definitions:
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
$ref: '#/definitions/ClientRes'
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- connected
|
||||
ChannelReq:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Free-form channel name.
|
||||
connected:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
ClientList:
|
||||
type: object
|
||||
properties:
|
||||
@ -450,11 +499,9 @@ definitions:
|
||||
key:
|
||||
type: string
|
||||
description: Auto-generated access key.
|
||||
meta:
|
||||
type: object
|
||||
description: Client's meta-data.
|
||||
additionalProperties:
|
||||
type: string
|
||||
payload:
|
||||
type: string
|
||||
description: Arbitrary, string-encoded client's data.
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
@ -471,11 +518,9 @@ definitions:
|
||||
name:
|
||||
type: string
|
||||
description: Free-form client name.
|
||||
meta:
|
||||
type: object
|
||||
description: Client's meta-data.
|
||||
additionalProperties:
|
||||
type: string
|
||||
payload:
|
||||
type: string
|
||||
description: Arbitrary, string-encoded client's data.
|
||||
required:
|
||||
- type
|
||||
Token:
|
||||
|
@ -5,8 +5,8 @@ import "github.com/asaskevich/govalidator"
|
||||
// User represents a Mainflux user account. Each user is identified given its
|
||||
// email and password.
|
||||
type User struct {
|
||||
Email string
|
||||
Password string
|
||||
Email string `gorm:"type:varchar(254);primary_key"`
|
||||
Password string `gorm:"type:char(60)"`
|
||||
}
|
||||
|
||||
// Validate returns an error if user representation is invalid.
|
||||
|
@ -1,5 +1,4 @@
|
||||
// Package writer provides message writer concept definitions.
|
||||
package writer
|
||||
package mainflux
|
||||
|
||||
// Message represents a resolved (normalized) raw message.
|
||||
type Message struct {
|
||||
@ -27,9 +26,9 @@ type RawMessage struct {
|
||||
Payload []byte `json:"payload"`
|
||||
}
|
||||
|
||||
// MessageRepository specifies a message persistence API.
|
||||
type MessageRepository interface {
|
||||
// Save persists the message. A non-nil error is returned to indicate
|
||||
// MessagePublisher specifies a message publishing API.
|
||||
type MessagePublisher interface {
|
||||
// Publishes message to the stream. A non-nil error is returned to indicate
|
||||
// operation failure.
|
||||
Save(RawMessage) error
|
||||
Publish(RawMessage) error
|
||||
}
|
47
normalizer/README.md
Normal file
47
normalizer/README.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Message normalizer
|
||||
|
||||
Normalizer service consumes events published by adapters, normalizes SenML-formatted
|
||||
ones, and publishes them to the post-processing stream.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|--------------------|------------------------------|-----------------------|
|
||||
| MF_NATS_URL | NATS instance URL | nats://localhost:4222 |
|
||||
| MF_NORMALIZER_PORT | Normalizer service HTTP port | 8180 |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. The following snippet
|
||||
provides a compose file template that can be used to deploy the service container
|
||||
locally:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
services:
|
||||
manager:
|
||||
image: mainflux/normalizer:[version]
|
||||
container_name: [instance name]
|
||||
environment:
|
||||
MF_NATS_URL: [NATS instance URL]
|
||||
MF_NORMALIZER_PORT: [Service HTTP port]
|
||||
```
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
go get github.com/mainflux/mainflux
|
||||
|
||||
cd $GOPATH/src/github.com/mainflux/mainflux/cmd/normalizer
|
||||
|
||||
# compile the app; make sure to set the proper GOOS value
|
||||
CGO_ENABLED=0 GOOS=[platform identifier] go build -ldflags "-s" -a -installsuffix cgo -o app
|
||||
|
||||
# set the environment variables and run the service
|
||||
MF_NATS_URL=[NATS instance URL] MF_NORMALIZER_PORT=[Service HTTP port] app
|
||||
```
|
18
normalizer/api.go
Normal file
18
normalizer/api.go
Normal file
@ -0,0 +1,18 @@
|
||||
package normalizer
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-zoo/bone"
|
||||
"github.com/mainflux/mainflux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler() http.Handler {
|
||||
r := bone.New()
|
||||
r.GetFunc("/version", mainflux.Version())
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
3
normalizer/doc.go
Normal file
3
normalizer/doc.go
Normal file
@ -0,0 +1,3 @@
|
||||
// Package normalizer contains the domain concept definitions needed to
|
||||
// support Mainflux normalizer service functionality.
|
||||
package normalizer
|
112
normalizer/normalizer.go
Normal file
112
normalizer/normalizer.go
Normal file
@ -0,0 +1,112 @@
|
||||
package normalizer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/cisco/senml"
|
||||
"github.com/go-kit/kit/log"
|
||||
"github.com/mainflux/mainflux"
|
||||
nats "github.com/nats-io/go-nats"
|
||||
)
|
||||
|
||||
const (
|
||||
queue string = "normalizers"
|
||||
subject string = "src.*"
|
||||
output string = "normalized"
|
||||
)
|
||||
|
||||
type eventFlow struct {
|
||||
nc *nats.Conn
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// Subscribe instantiates and starts a new NATS message flow.
|
||||
func Subscribe(nc *nats.Conn, logger log.Logger) {
|
||||
flow := eventFlow{nc, logger}
|
||||
flow.start()
|
||||
}
|
||||
|
||||
func (ef eventFlow) start() {
|
||||
ef.nc.QueueSubscribe(subject, queue, func(m *nats.Msg) {
|
||||
msg := mainflux.RawMessage{}
|
||||
|
||||
if err := json.Unmarshal(m.Data, &msg); err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Unmarshalling failed: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := ef.publish(msg); err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Publishing failed: %s", err))
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (ef eventFlow) publish(msg mainflux.RawMessage) error {
|
||||
normalized, err := ef.normalize(msg)
|
||||
if err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Normalization failed: %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range normalized {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Marshalling failed: %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ef.nc.Publish(subject, data); err != nil {
|
||||
ef.logger.Log("error", fmt.Sprintf("Publishing failed: %s", err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ef eventFlow) normalize(msg mainflux.RawMessage) ([]mainflux.Message, error) {
|
||||
var (
|
||||
raw, normalized senml.SenML
|
||||
err error
|
||||
)
|
||||
|
||||
if raw, err = senml.Decode(msg.Payload, senml.JSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
normalized = senml.Normalize(raw)
|
||||
|
||||
msgs := make([]mainflux.Message, len(normalized.Records))
|
||||
for k, v := range normalized.Records {
|
||||
m := mainflux.Message{
|
||||
Channel: msg.Channel,
|
||||
Publisher: msg.Publisher,
|
||||
Protocol: msg.Protocol,
|
||||
Name: v.Name,
|
||||
Unit: v.Unit,
|
||||
StringValue: v.StringValue,
|
||||
DataValue: v.DataValue,
|
||||
Time: v.Time,
|
||||
UpdateTime: v.UpdateTime,
|
||||
Link: v.Link,
|
||||
}
|
||||
|
||||
if v.Value != nil {
|
||||
m.Value = *v.Value
|
||||
}
|
||||
|
||||
if v.BoolValue != nil {
|
||||
m.BoolValue = *v.BoolValue
|
||||
}
|
||||
|
||||
if v.Sum != nil {
|
||||
m.ValueSum = *v.Sum
|
||||
}
|
||||
|
||||
msgs[k] = m
|
||||
}
|
||||
|
||||
return msgs, nil
|
||||
}
|
13
utils.go
Normal file
13
utils.go
Normal file
@ -0,0 +1,13 @@
|
||||
package mainflux
|
||||
|
||||
import "os"
|
||||
|
||||
// Env reads specified environment variable. If no value has been found,
|
||||
// fallback is returned.
|
||||
func Env(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
21
vendor/github.com/Azure/go-ansiterm/LICENSE
generated
vendored
Normal file
21
vendor/github.com/Azure/go-ansiterm/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Microsoft Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
12
vendor/github.com/Azure/go-ansiterm/README.md
generated
vendored
Normal file
12
vendor/github.com/Azure/go-ansiterm/README.md
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# go-ansiterm
|
||||
|
||||
This is a cross platform Ansi Terminal Emulation library. It reads a stream of Ansi characters and produces the appropriate function calls. The results of the function calls are platform dependent.
|
||||
|
||||
For example the parser might receive "ESC, [, A" as a stream of three characters. This is the code for Cursor Up (http://www.vt100.net/docs/vt510-rm/CUU). The parser then calls the cursor up function (CUU()) on an event handler. The event handler determines what platform specific work must be done to cause the cursor to move up one position.
|
||||
|
||||
The parser (parser.go) is a partial implementation of this state machine (http://vt100.net/emu/vt500_parser.png). There are also two event handler implementations, one for tests (test_event_handler.go) to validate that the expected events are being produced and called, the other is a Windows implementation (winterm/win_event_handler.go).
|
||||
|
||||
See parser_test.go for examples exercising the state machine and generating appropriate function calls.
|
||||
|
||||
-----
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
188
vendor/github.com/Azure/go-ansiterm/constants.go
generated
vendored
Normal file
188
vendor/github.com/Azure/go-ansiterm/constants.go
generated
vendored
Normal file
@ -0,0 +1,188 @@
|
||||
package ansiterm
|
||||
|
||||
const LogEnv = "DEBUG_TERMINAL"
|
||||
|
||||
// ANSI constants
|
||||
// References:
|
||||
// -- http://www.ecma-international.org/publications/standards/Ecma-048.htm
|
||||
// -- http://man7.org/linux/man-pages/man4/console_codes.4.html
|
||||
// -- http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
|
||||
// -- http://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
// -- http://vt100.net/emu/dec_ansi_parser
|
||||
// -- http://vt100.net/emu/vt500_parser.svg
|
||||
// -- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
// -- http://www.inwap.com/pdp10/ansicode.txt
|
||||
const (
|
||||
// ECMA-48 Set Graphics Rendition
|
||||
// Note:
|
||||
// -- Constants leading with an underscore (e.g., _ANSI_xxx) are unsupported or reserved
|
||||
// -- Fonts could possibly be supported via SetCurrentConsoleFontEx
|
||||
// -- Windows does not expose the per-window cursor (i.e., caret) blink times
|
||||
ANSI_SGR_RESET = 0
|
||||
ANSI_SGR_BOLD = 1
|
||||
ANSI_SGR_DIM = 2
|
||||
_ANSI_SGR_ITALIC = 3
|
||||
ANSI_SGR_UNDERLINE = 4
|
||||
_ANSI_SGR_BLINKSLOW = 5
|
||||
_ANSI_SGR_BLINKFAST = 6
|
||||
ANSI_SGR_REVERSE = 7
|
||||
_ANSI_SGR_INVISIBLE = 8
|
||||
_ANSI_SGR_LINETHROUGH = 9
|
||||
_ANSI_SGR_FONT_00 = 10
|
||||
_ANSI_SGR_FONT_01 = 11
|
||||
_ANSI_SGR_FONT_02 = 12
|
||||
_ANSI_SGR_FONT_03 = 13
|
||||
_ANSI_SGR_FONT_04 = 14
|
||||
_ANSI_SGR_FONT_05 = 15
|
||||
_ANSI_SGR_FONT_06 = 16
|
||||
_ANSI_SGR_FONT_07 = 17
|
||||
_ANSI_SGR_FONT_08 = 18
|
||||
_ANSI_SGR_FONT_09 = 19
|
||||
_ANSI_SGR_FONT_10 = 20
|
||||
_ANSI_SGR_DOUBLEUNDERLINE = 21
|
||||
ANSI_SGR_BOLD_DIM_OFF = 22
|
||||
_ANSI_SGR_ITALIC_OFF = 23
|
||||
ANSI_SGR_UNDERLINE_OFF = 24
|
||||
_ANSI_SGR_BLINK_OFF = 25
|
||||
_ANSI_SGR_RESERVED_00 = 26
|
||||
ANSI_SGR_REVERSE_OFF = 27
|
||||
_ANSI_SGR_INVISIBLE_OFF = 28
|
||||
_ANSI_SGR_LINETHROUGH_OFF = 29
|
||||
ANSI_SGR_FOREGROUND_BLACK = 30
|
||||
ANSI_SGR_FOREGROUND_RED = 31
|
||||
ANSI_SGR_FOREGROUND_GREEN = 32
|
||||
ANSI_SGR_FOREGROUND_YELLOW = 33
|
||||
ANSI_SGR_FOREGROUND_BLUE = 34
|
||||
ANSI_SGR_FOREGROUND_MAGENTA = 35
|
||||
ANSI_SGR_FOREGROUND_CYAN = 36
|
||||
ANSI_SGR_FOREGROUND_WHITE = 37
|
||||
_ANSI_SGR_RESERVED_01 = 38
|
||||
ANSI_SGR_FOREGROUND_DEFAULT = 39
|
||||
ANSI_SGR_BACKGROUND_BLACK = 40
|
||||
ANSI_SGR_BACKGROUND_RED = 41
|
||||
ANSI_SGR_BACKGROUND_GREEN = 42
|
||||
ANSI_SGR_BACKGROUND_YELLOW = 43
|
||||
ANSI_SGR_BACKGROUND_BLUE = 44
|
||||
ANSI_SGR_BACKGROUND_MAGENTA = 45
|
||||
ANSI_SGR_BACKGROUND_CYAN = 46
|
||||
ANSI_SGR_BACKGROUND_WHITE = 47
|
||||
_ANSI_SGR_RESERVED_02 = 48
|
||||
ANSI_SGR_BACKGROUND_DEFAULT = 49
|
||||
// 50 - 65: Unsupported
|
||||
|
||||
ANSI_MAX_CMD_LENGTH = 4096
|
||||
|
||||
MAX_INPUT_EVENTS = 128
|
||||
DEFAULT_WIDTH = 80
|
||||
DEFAULT_HEIGHT = 24
|
||||
|
||||
ANSI_BEL = 0x07
|
||||
ANSI_BACKSPACE = 0x08
|
||||
ANSI_TAB = 0x09
|
||||
ANSI_LINE_FEED = 0x0A
|
||||
ANSI_VERTICAL_TAB = 0x0B
|
||||
ANSI_FORM_FEED = 0x0C
|
||||
ANSI_CARRIAGE_RETURN = 0x0D
|
||||
ANSI_ESCAPE_PRIMARY = 0x1B
|
||||
ANSI_ESCAPE_SECONDARY = 0x5B
|
||||
ANSI_OSC_STRING_ENTRY = 0x5D
|
||||
ANSI_COMMAND_FIRST = 0x40
|
||||
ANSI_COMMAND_LAST = 0x7E
|
||||
DCS_ENTRY = 0x90
|
||||
CSI_ENTRY = 0x9B
|
||||
OSC_STRING = 0x9D
|
||||
ANSI_PARAMETER_SEP = ";"
|
||||
ANSI_CMD_G0 = '('
|
||||
ANSI_CMD_G1 = ')'
|
||||
ANSI_CMD_G2 = '*'
|
||||
ANSI_CMD_G3 = '+'
|
||||
ANSI_CMD_DECPNM = '>'
|
||||
ANSI_CMD_DECPAM = '='
|
||||
ANSI_CMD_OSC = ']'
|
||||
ANSI_CMD_STR_TERM = '\\'
|
||||
|
||||
KEY_CONTROL_PARAM_2 = ";2"
|
||||
KEY_CONTROL_PARAM_3 = ";3"
|
||||
KEY_CONTROL_PARAM_4 = ";4"
|
||||
KEY_CONTROL_PARAM_5 = ";5"
|
||||
KEY_CONTROL_PARAM_6 = ";6"
|
||||
KEY_CONTROL_PARAM_7 = ";7"
|
||||
KEY_CONTROL_PARAM_8 = ";8"
|
||||
KEY_ESC_CSI = "\x1B["
|
||||
KEY_ESC_N = "\x1BN"
|
||||
KEY_ESC_O = "\x1BO"
|
||||
|
||||
FILL_CHARACTER = ' '
|
||||
)
|
||||
|
||||
func getByteRange(start byte, end byte) []byte {
|
||||
bytes := make([]byte, 0, 32)
|
||||
for i := start; i <= end; i++ {
|
||||
bytes = append(bytes, byte(i))
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
var toGroundBytes = getToGroundBytes()
|
||||
var executors = getExecuteBytes()
|
||||
|
||||
// SPACE 20+A0 hex Always and everywhere a blank space
|
||||
// Intermediate 20-2F hex !"#$%&'()*+,-./
|
||||
var intermeds = getByteRange(0x20, 0x2F)
|
||||
|
||||
// Parameters 30-3F hex 0123456789:;<=>?
|
||||
// CSI Parameters 30-39, 3B hex 0123456789;
|
||||
var csiParams = getByteRange(0x30, 0x3F)
|
||||
|
||||
var csiCollectables = append(getByteRange(0x30, 0x39), getByteRange(0x3B, 0x3F)...)
|
||||
|
||||
// Uppercase 40-5F hex @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
|
||||
var upperCase = getByteRange(0x40, 0x5F)
|
||||
|
||||
// Lowercase 60-7E hex `abcdefghijlkmnopqrstuvwxyz{|}~
|
||||
var lowerCase = getByteRange(0x60, 0x7E)
|
||||
|
||||
// Alphabetics 40-7E hex (all of upper and lower case)
|
||||
var alphabetics = append(upperCase, lowerCase...)
|
||||
|
||||
var printables = getByteRange(0x20, 0x7F)
|
||||
|
||||
var escapeIntermediateToGroundBytes = getByteRange(0x30, 0x7E)
|
||||
var escapeToGroundBytes = getEscapeToGroundBytes()
|
||||
|
||||
// See http://www.vt100.net/emu/vt500_parser.png for description of the complex
|
||||
// byte ranges below
|
||||
|
||||
func getEscapeToGroundBytes() []byte {
|
||||
escapeToGroundBytes := getByteRange(0x30, 0x4F)
|
||||
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x51, 0x57)...)
|
||||
escapeToGroundBytes = append(escapeToGroundBytes, 0x59)
|
||||
escapeToGroundBytes = append(escapeToGroundBytes, 0x5A)
|
||||
escapeToGroundBytes = append(escapeToGroundBytes, 0x5C)
|
||||
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x60, 0x7E)...)
|
||||
return escapeToGroundBytes
|
||||
}
|
||||
|
||||
func getExecuteBytes() []byte {
|
||||
executeBytes := getByteRange(0x00, 0x17)
|
||||
executeBytes = append(executeBytes, 0x19)
|
||||
executeBytes = append(executeBytes, getByteRange(0x1C, 0x1F)...)
|
||||
return executeBytes
|
||||
}
|
||||
|
||||
func getToGroundBytes() []byte {
|
||||
groundBytes := []byte{0x18}
|
||||
groundBytes = append(groundBytes, 0x1A)
|
||||
groundBytes = append(groundBytes, getByteRange(0x80, 0x8F)...)
|
||||
groundBytes = append(groundBytes, getByteRange(0x91, 0x97)...)
|
||||
groundBytes = append(groundBytes, 0x99)
|
||||
groundBytes = append(groundBytes, 0x9A)
|
||||
groundBytes = append(groundBytes, 0x9C)
|
||||
return groundBytes
|
||||
}
|
||||
|
||||
// Delete 7F hex Always and everywhere ignored
|
||||
// C1 Control 80-9F hex 32 additional control characters
|
||||
// G1 Displayable A1-FE hex 94 additional displayable characters
|
||||
// Special A0+FF hex Same as SPACE and DELETE
|
7
vendor/github.com/Azure/go-ansiterm/context.go
generated
vendored
Normal file
7
vendor/github.com/Azure/go-ansiterm/context.go
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
package ansiterm
|
||||
|
||||
type ansiContext struct {
|
||||
currentChar byte
|
||||
paramBuffer []byte
|
||||
interBuffer []byte
|
||||
}
|
49
vendor/github.com/Azure/go-ansiterm/csi_entry_state.go
generated
vendored
Normal file
49
vendor/github.com/Azure/go-ansiterm/csi_entry_state.go
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
package ansiterm
|
||||
|
||||
type csiEntryState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (csiState csiEntryState) Handle(b byte) (s state, e error) {
|
||||
csiState.parser.logf("CsiEntry::Handle %#x", b)
|
||||
|
||||
nextState, err := csiState.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(alphabetics, b):
|
||||
return csiState.parser.ground, nil
|
||||
case sliceContains(csiCollectables, b):
|
||||
return csiState.parser.csiParam, nil
|
||||
case sliceContains(executors, b):
|
||||
return csiState, csiState.parser.execute()
|
||||
}
|
||||
|
||||
return csiState, nil
|
||||
}
|
||||
|
||||
func (csiState csiEntryState) Transition(s state) error {
|
||||
csiState.parser.logf("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name())
|
||||
csiState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case csiState.parser.ground:
|
||||
return csiState.parser.csiDispatch()
|
||||
case csiState.parser.csiParam:
|
||||
switch {
|
||||
case sliceContains(csiParams, csiState.parser.context.currentChar):
|
||||
csiState.parser.collectParam()
|
||||
case sliceContains(intermeds, csiState.parser.context.currentChar):
|
||||
csiState.parser.collectInter()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (csiState csiEntryState) Enter() error {
|
||||
csiState.parser.clear()
|
||||
return nil
|
||||
}
|
38
vendor/github.com/Azure/go-ansiterm/csi_param_state.go
generated
vendored
Normal file
38
vendor/github.com/Azure/go-ansiterm/csi_param_state.go
generated
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
package ansiterm
|
||||
|
||||
type csiParamState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (csiState csiParamState) Handle(b byte) (s state, e error) {
|
||||
csiState.parser.logf("CsiParam::Handle %#x", b)
|
||||
|
||||
nextState, err := csiState.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(alphabetics, b):
|
||||
return csiState.parser.ground, nil
|
||||
case sliceContains(csiCollectables, b):
|
||||
csiState.parser.collectParam()
|
||||
return csiState, nil
|
||||
case sliceContains(executors, b):
|
||||
return csiState, csiState.parser.execute()
|
||||
}
|
||||
|
||||
return csiState, nil
|
||||
}
|
||||
|
||||
func (csiState csiParamState) Transition(s state) error {
|
||||
csiState.parser.logf("CsiParam::Transition %s --> %s", csiState.Name(), s.Name())
|
||||
csiState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case csiState.parser.ground:
|
||||
return csiState.parser.csiDispatch()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
36
vendor/github.com/Azure/go-ansiterm/escape_intermediate_state.go
generated
vendored
Normal file
36
vendor/github.com/Azure/go-ansiterm/escape_intermediate_state.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
package ansiterm
|
||||
|
||||
type escapeIntermediateState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (escState escapeIntermediateState) Handle(b byte) (s state, e error) {
|
||||
escState.parser.logf("escapeIntermediateState::Handle %#x", b)
|
||||
nextState, err := escState.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(intermeds, b):
|
||||
return escState, escState.parser.collectInter()
|
||||
case sliceContains(executors, b):
|
||||
return escState, escState.parser.execute()
|
||||
case sliceContains(escapeIntermediateToGroundBytes, b):
|
||||
return escState.parser.ground, nil
|
||||
}
|
||||
|
||||
return escState, nil
|
||||
}
|
||||
|
||||
func (escState escapeIntermediateState) Transition(s state) error {
|
||||
escState.parser.logf("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name())
|
||||
escState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case escState.parser.ground:
|
||||
return escState.parser.escDispatch()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
47
vendor/github.com/Azure/go-ansiterm/escape_state.go
generated
vendored
Normal file
47
vendor/github.com/Azure/go-ansiterm/escape_state.go
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
package ansiterm
|
||||
|
||||
type escapeState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (escState escapeState) Handle(b byte) (s state, e error) {
|
||||
escState.parser.logf("escapeState::Handle %#x", b)
|
||||
nextState, err := escState.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case b == ANSI_ESCAPE_SECONDARY:
|
||||
return escState.parser.csiEntry, nil
|
||||
case b == ANSI_OSC_STRING_ENTRY:
|
||||
return escState.parser.oscString, nil
|
||||
case sliceContains(executors, b):
|
||||
return escState, escState.parser.execute()
|
||||
case sliceContains(escapeToGroundBytes, b):
|
||||
return escState.parser.ground, nil
|
||||
case sliceContains(intermeds, b):
|
||||
return escState.parser.escapeIntermediate, nil
|
||||
}
|
||||
|
||||
return escState, nil
|
||||
}
|
||||
|
||||
func (escState escapeState) Transition(s state) error {
|
||||
escState.parser.logf("Escape::Transition %s --> %s", escState.Name(), s.Name())
|
||||
escState.baseState.Transition(s)
|
||||
|
||||
switch s {
|
||||
case escState.parser.ground:
|
||||
return escState.parser.escDispatch()
|
||||
case escState.parser.escapeIntermediate:
|
||||
return escState.parser.collectInter()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (escState escapeState) Enter() error {
|
||||
escState.parser.clear()
|
||||
return nil
|
||||
}
|
90
vendor/github.com/Azure/go-ansiterm/event_handler.go
generated
vendored
Normal file
90
vendor/github.com/Azure/go-ansiterm/event_handler.go
generated
vendored
Normal file
@ -0,0 +1,90 @@
|
||||
package ansiterm
|
||||
|
||||
type AnsiEventHandler interface {
|
||||
// Print
|
||||
Print(b byte) error
|
||||
|
||||
// Execute C0 commands
|
||||
Execute(b byte) error
|
||||
|
||||
// CUrsor Up
|
||||
CUU(int) error
|
||||
|
||||
// CUrsor Down
|
||||
CUD(int) error
|
||||
|
||||
// CUrsor Forward
|
||||
CUF(int) error
|
||||
|
||||
// CUrsor Backward
|
||||
CUB(int) error
|
||||
|
||||
// Cursor to Next Line
|
||||
CNL(int) error
|
||||
|
||||
// Cursor to Previous Line
|
||||
CPL(int) error
|
||||
|
||||
// Cursor Horizontal position Absolute
|
||||
CHA(int) error
|
||||
|
||||
// Vertical line Position Absolute
|
||||
VPA(int) error
|
||||
|
||||
// CUrsor Position
|
||||
CUP(int, int) error
|
||||
|
||||
// Horizontal and Vertical Position (depends on PUM)
|
||||
HVP(int, int) error
|
||||
|
||||
// Text Cursor Enable Mode
|
||||
DECTCEM(bool) error
|
||||
|
||||
// Origin Mode
|
||||
DECOM(bool) error
|
||||
|
||||
// 132 Column Mode
|
||||
DECCOLM(bool) error
|
||||
|
||||
// Erase in Display
|
||||
ED(int) error
|
||||
|
||||
// Erase in Line
|
||||
EL(int) error
|
||||
|
||||
// Insert Line
|
||||
IL(int) error
|
||||
|
||||
// Delete Line
|
||||
DL(int) error
|
||||
|
||||
// Insert Character
|
||||
ICH(int) error
|
||||
|
||||
// Delete Character
|
||||
DCH(int) error
|
||||
|
||||
// Set Graphics Rendition
|
||||
SGR([]int) error
|
||||
|
||||
// Pan Down
|
||||
SU(int) error
|
||||
|
||||
// Pan Up
|
||||
SD(int) error
|
||||
|
||||
// Device Attributes
|
||||
DA([]string) error
|
||||
|
||||
// Set Top and Bottom Margins
|
||||
DECSTBM(int, int) error
|
||||
|
||||
// Index
|
||||
IND() error
|
||||
|
||||
// Reverse Index
|
||||
RI() error
|
||||
|
||||
// Flush updates from previous commands
|
||||
Flush() error
|
||||
}
|
24
vendor/github.com/Azure/go-ansiterm/ground_state.go
generated
vendored
Normal file
24
vendor/github.com/Azure/go-ansiterm/ground_state.go
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
package ansiterm
|
||||
|
||||
type groundState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (gs groundState) Handle(b byte) (s state, e error) {
|
||||
gs.parser.context.currentChar = b
|
||||
|
||||
nextState, err := gs.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case sliceContains(printables, b):
|
||||
return gs, gs.parser.print()
|
||||
|
||||
case sliceContains(executors, b):
|
||||
return gs, gs.parser.execute()
|
||||
}
|
||||
|
||||
return gs, nil
|
||||
}
|
31
vendor/github.com/Azure/go-ansiterm/osc_string_state.go
generated
vendored
Normal file
31
vendor/github.com/Azure/go-ansiterm/osc_string_state.go
generated
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
package ansiterm
|
||||
|
||||
type oscStringState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
func (oscState oscStringState) Handle(b byte) (s state, e error) {
|
||||
oscState.parser.logf("OscString::Handle %#x", b)
|
||||
nextState, err := oscState.baseState.Handle(b)
|
||||
if nextState != nil || err != nil {
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case isOscStringTerminator(b):
|
||||
return oscState.parser.ground, nil
|
||||
}
|
||||
|
||||
return oscState, nil
|
||||
}
|
||||
|
||||
// See below for OSC string terminators for linux
|
||||
// http://man7.org/linux/man-pages/man4/console_codes.4.html
|
||||
func isOscStringTerminator(b byte) bool {
|
||||
|
||||
if b == ANSI_BEL || b == 0x5C {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
151
vendor/github.com/Azure/go-ansiterm/parser.go
generated
vendored
Normal file
151
vendor/github.com/Azure/go-ansiterm/parser.go
generated
vendored
Normal file
@ -0,0 +1,151 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type AnsiParser struct {
|
||||
currState state
|
||||
eventHandler AnsiEventHandler
|
||||
context *ansiContext
|
||||
csiEntry state
|
||||
csiParam state
|
||||
dcsEntry state
|
||||
escape state
|
||||
escapeIntermediate state
|
||||
error state
|
||||
ground state
|
||||
oscString state
|
||||
stateMap []state
|
||||
|
||||
logf func(string, ...interface{})
|
||||
}
|
||||
|
||||
type Option func(*AnsiParser)
|
||||
|
||||
func WithLogf(f func(string, ...interface{})) Option {
|
||||
return func(ap *AnsiParser) {
|
||||
ap.logf = f
|
||||
}
|
||||
}
|
||||
|
||||
func CreateParser(initialState string, evtHandler AnsiEventHandler, opts ...Option) *AnsiParser {
|
||||
ap := &AnsiParser{
|
||||
eventHandler: evtHandler,
|
||||
context: &ansiContext{},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(ap)
|
||||
}
|
||||
|
||||
if isDebugEnv := os.Getenv(LogEnv); isDebugEnv == "1" {
|
||||
logFile, _ := os.Create("ansiParser.log")
|
||||
logger := log.New(logFile, "", log.LstdFlags)
|
||||
if ap.logf != nil {
|
||||
l := ap.logf
|
||||
ap.logf = func(s string, v ...interface{}) {
|
||||
l(s, v...)
|
||||
logger.Printf(s, v...)
|
||||
}
|
||||
} else {
|
||||
ap.logf = logger.Printf
|
||||
}
|
||||
}
|
||||
|
||||
if ap.logf == nil {
|
||||
ap.logf = func(string, ...interface{}) {}
|
||||
}
|
||||
|
||||
ap.csiEntry = csiEntryState{baseState{name: "CsiEntry", parser: ap}}
|
||||
ap.csiParam = csiParamState{baseState{name: "CsiParam", parser: ap}}
|
||||
ap.dcsEntry = dcsEntryState{baseState{name: "DcsEntry", parser: ap}}
|
||||
ap.escape = escapeState{baseState{name: "Escape", parser: ap}}
|
||||
ap.escapeIntermediate = escapeIntermediateState{baseState{name: "EscapeIntermediate", parser: ap}}
|
||||
ap.error = errorState{baseState{name: "Error", parser: ap}}
|
||||
ap.ground = groundState{baseState{name: "Ground", parser: ap}}
|
||||
ap.oscString = oscStringState{baseState{name: "OscString", parser: ap}}
|
||||
|
||||
ap.stateMap = []state{
|
||||
ap.csiEntry,
|
||||
ap.csiParam,
|
||||
ap.dcsEntry,
|
||||
ap.escape,
|
||||
ap.escapeIntermediate,
|
||||
ap.error,
|
||||
ap.ground,
|
||||
ap.oscString,
|
||||
}
|
||||
|
||||
ap.currState = getState(initialState, ap.stateMap)
|
||||
|
||||
ap.logf("CreateParser: parser %p", ap)
|
||||
return ap
|
||||
}
|
||||
|
||||
func getState(name string, states []state) state {
|
||||
for _, el := range states {
|
||||
if el.Name() == name {
|
||||
return el
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) Parse(bytes []byte) (int, error) {
|
||||
for i, b := range bytes {
|
||||
if err := ap.handle(b); err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
|
||||
return len(bytes), ap.eventHandler.Flush()
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) handle(b byte) error {
|
||||
ap.context.currentChar = b
|
||||
newState, err := ap.currState.Handle(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newState == nil {
|
||||
ap.logf("WARNING: newState is nil")
|
||||
return errors.New("New state of 'nil' is invalid.")
|
||||
}
|
||||
|
||||
if newState != ap.currState {
|
||||
if err := ap.changeState(newState); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) changeState(newState state) error {
|
||||
ap.logf("ChangeState %s --> %s", ap.currState.Name(), newState.Name())
|
||||
|
||||
// Exit old state
|
||||
if err := ap.currState.Exit(); err != nil {
|
||||
ap.logf("Exit state '%s' failed with : '%v'", ap.currState.Name(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform transition action
|
||||
if err := ap.currState.Transition(newState); err != nil {
|
||||
ap.logf("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Enter new state
|
||||
if err := newState.Enter(); err != nil {
|
||||
ap.logf("Enter state '%s' failed with: '%v'", newState.Name(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
ap.currState = newState
|
||||
return nil
|
||||
}
|
99
vendor/github.com/Azure/go-ansiterm/parser_action_helpers.go
generated
vendored
Normal file
99
vendor/github.com/Azure/go-ansiterm/parser_action_helpers.go
generated
vendored
Normal file
@ -0,0 +1,99 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func parseParams(bytes []byte) ([]string, error) {
|
||||
paramBuff := make([]byte, 0, 0)
|
||||
params := []string{}
|
||||
|
||||
for _, v := range bytes {
|
||||
if v == ';' {
|
||||
if len(paramBuff) > 0 {
|
||||
// Completed parameter, append it to the list
|
||||
s := string(paramBuff)
|
||||
params = append(params, s)
|
||||
paramBuff = make([]byte, 0, 0)
|
||||
}
|
||||
} else {
|
||||
paramBuff = append(paramBuff, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Last parameter may not be terminated with ';'
|
||||
if len(paramBuff) > 0 {
|
||||
s := string(paramBuff)
|
||||
params = append(params, s)
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func parseCmd(context ansiContext) (string, error) {
|
||||
return string(context.currentChar), nil
|
||||
}
|
||||
|
||||
func getInt(params []string, dflt int) int {
|
||||
i := getInts(params, 1, dflt)[0]
|
||||
return i
|
||||
}
|
||||
|
||||
func getInts(params []string, minCount int, dflt int) []int {
|
||||
ints := []int{}
|
||||
|
||||
for _, v := range params {
|
||||
i, _ := strconv.Atoi(v)
|
||||
// Zero is mapped to the default value in VT100.
|
||||
if i == 0 {
|
||||
i = dflt
|
||||
}
|
||||
ints = append(ints, i)
|
||||
}
|
||||
|
||||
if len(ints) < minCount {
|
||||
remaining := minCount - len(ints)
|
||||
for i := 0; i < remaining; i++ {
|
||||
ints = append(ints, dflt)
|
||||
}
|
||||
}
|
||||
|
||||
return ints
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) modeDispatch(param string, set bool) error {
|
||||
switch param {
|
||||
case "?3":
|
||||
return ap.eventHandler.DECCOLM(set)
|
||||
case "?6":
|
||||
return ap.eventHandler.DECOM(set)
|
||||
case "?25":
|
||||
return ap.eventHandler.DECTCEM(set)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) hDispatch(params []string) error {
|
||||
if len(params) == 1 {
|
||||
return ap.modeDispatch(params[0], true)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) lDispatch(params []string) error {
|
||||
if len(params) == 1 {
|
||||
return ap.modeDispatch(params[0], false)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEraseParam(params []string) int {
|
||||
param := getInt(params, 0)
|
||||
if param < 0 || 3 < param {
|
||||
param = 0
|
||||
}
|
||||
|
||||
return param
|
||||
}
|
119
vendor/github.com/Azure/go-ansiterm/parser_actions.go
generated
vendored
Normal file
119
vendor/github.com/Azure/go-ansiterm/parser_actions.go
generated
vendored
Normal file
@ -0,0 +1,119 @@
|
||||
package ansiterm
|
||||
|
||||
func (ap *AnsiParser) collectParam() error {
|
||||
currChar := ap.context.currentChar
|
||||
ap.logf("collectParam %#x", currChar)
|
||||
ap.context.paramBuffer = append(ap.context.paramBuffer, currChar)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) collectInter() error {
|
||||
currChar := ap.context.currentChar
|
||||
ap.logf("collectInter %#x", currChar)
|
||||
ap.context.paramBuffer = append(ap.context.interBuffer, currChar)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) escDispatch() error {
|
||||
cmd, _ := parseCmd(*ap.context)
|
||||
intermeds := ap.context.interBuffer
|
||||
ap.logf("escDispatch currentChar: %#x", ap.context.currentChar)
|
||||
ap.logf("escDispatch: %v(%v)", cmd, intermeds)
|
||||
|
||||
switch cmd {
|
||||
case "D": // IND
|
||||
return ap.eventHandler.IND()
|
||||
case "E": // NEL, equivalent to CRLF
|
||||
err := ap.eventHandler.Execute(ANSI_CARRIAGE_RETURN)
|
||||
if err == nil {
|
||||
err = ap.eventHandler.Execute(ANSI_LINE_FEED)
|
||||
}
|
||||
return err
|
||||
case "M": // RI
|
||||
return ap.eventHandler.RI()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) csiDispatch() error {
|
||||
cmd, _ := parseCmd(*ap.context)
|
||||
params, _ := parseParams(ap.context.paramBuffer)
|
||||
ap.logf("Parsed params: %v with length: %d", params, len(params))
|
||||
|
||||
ap.logf("csiDispatch: %v(%v)", cmd, params)
|
||||
|
||||
switch cmd {
|
||||
case "@":
|
||||
return ap.eventHandler.ICH(getInt(params, 1))
|
||||
case "A":
|
||||
return ap.eventHandler.CUU(getInt(params, 1))
|
||||
case "B":
|
||||
return ap.eventHandler.CUD(getInt(params, 1))
|
||||
case "C":
|
||||
return ap.eventHandler.CUF(getInt(params, 1))
|
||||
case "D":
|
||||
return ap.eventHandler.CUB(getInt(params, 1))
|
||||
case "E":
|
||||
return ap.eventHandler.CNL(getInt(params, 1))
|
||||
case "F":
|
||||
return ap.eventHandler.CPL(getInt(params, 1))
|
||||
case "G":
|
||||
return ap.eventHandler.CHA(getInt(params, 1))
|
||||
case "H":
|
||||
ints := getInts(params, 2, 1)
|
||||
x, y := ints[0], ints[1]
|
||||
return ap.eventHandler.CUP(x, y)
|
||||
case "J":
|
||||
param := getEraseParam(params)
|
||||
return ap.eventHandler.ED(param)
|
||||
case "K":
|
||||
param := getEraseParam(params)
|
||||
return ap.eventHandler.EL(param)
|
||||
case "L":
|
||||
return ap.eventHandler.IL(getInt(params, 1))
|
||||
case "M":
|
||||
return ap.eventHandler.DL(getInt(params, 1))
|
||||
case "P":
|
||||
return ap.eventHandler.DCH(getInt(params, 1))
|
||||
case "S":
|
||||
return ap.eventHandler.SU(getInt(params, 1))
|
||||
case "T":
|
||||
return ap.eventHandler.SD(getInt(params, 1))
|
||||
case "c":
|
||||
return ap.eventHandler.DA(params)
|
||||
case "d":
|
||||
return ap.eventHandler.VPA(getInt(params, 1))
|
||||
case "f":
|
||||
ints := getInts(params, 2, 1)
|
||||
x, y := ints[0], ints[1]
|
||||
return ap.eventHandler.HVP(x, y)
|
||||
case "h":
|
||||
return ap.hDispatch(params)
|
||||
case "l":
|
||||
return ap.lDispatch(params)
|
||||
case "m":
|
||||
return ap.eventHandler.SGR(getInts(params, 1, 0))
|
||||
case "r":
|
||||
ints := getInts(params, 2, 1)
|
||||
top, bottom := ints[0], ints[1]
|
||||
return ap.eventHandler.DECSTBM(top, bottom)
|
||||
default:
|
||||
ap.logf("ERROR: Unsupported CSI command: '%s', with full context: %v", cmd, ap.context)
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) print() error {
|
||||
return ap.eventHandler.Print(ap.context.currentChar)
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) clear() error {
|
||||
ap.context = &ansiContext{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnsiParser) execute() error {
|
||||
return ap.eventHandler.Execute(ap.context.currentChar)
|
||||
}
|
71
vendor/github.com/Azure/go-ansiterm/states.go
generated
vendored
Normal file
71
vendor/github.com/Azure/go-ansiterm/states.go
generated
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
package ansiterm
|
||||
|
||||
type stateID int
|
||||
|
||||
type state interface {
|
||||
Enter() error
|
||||
Exit() error
|
||||
Handle(byte) (state, error)
|
||||
Name() string
|
||||
Transition(state) error
|
||||
}
|
||||
|
||||
type baseState struct {
|
||||
name string
|
||||
parser *AnsiParser
|
||||
}
|
||||
|
||||
func (base baseState) Enter() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (base baseState) Exit() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (base baseState) Handle(b byte) (s state, e error) {
|
||||
|
||||
switch {
|
||||
case b == CSI_ENTRY:
|
||||
return base.parser.csiEntry, nil
|
||||
case b == DCS_ENTRY:
|
||||
return base.parser.dcsEntry, nil
|
||||
case b == ANSI_ESCAPE_PRIMARY:
|
||||
return base.parser.escape, nil
|
||||
case b == OSC_STRING:
|
||||
return base.parser.oscString, nil
|
||||
case sliceContains(toGroundBytes, b):
|
||||
return base.parser.ground, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (base baseState) Name() string {
|
||||
return base.name
|
||||
}
|
||||
|
||||
func (base baseState) Transition(s state) error {
|
||||
if s == base.parser.ground {
|
||||
execBytes := []byte{0x18}
|
||||
execBytes = append(execBytes, 0x1A)
|
||||
execBytes = append(execBytes, getByteRange(0x80, 0x8F)...)
|
||||
execBytes = append(execBytes, getByteRange(0x91, 0x97)...)
|
||||
execBytes = append(execBytes, 0x99)
|
||||
execBytes = append(execBytes, 0x9A)
|
||||
|
||||
if sliceContains(execBytes, base.parser.context.currentChar) {
|
||||
return base.parser.execute()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type dcsEntryState struct {
|
||||
baseState
|
||||
}
|
||||
|
||||
type errorState struct {
|
||||
baseState
|
||||
}
|
21
vendor/github.com/Azure/go-ansiterm/utilities.go
generated
vendored
Normal file
21
vendor/github.com/Azure/go-ansiterm/utilities.go
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func sliceContains(bytes []byte, b byte) bool {
|
||||
for _, v := range bytes {
|
||||
if v == b {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func convertBytesToInteger(bytes []byte) int {
|
||||
s := string(bytes)
|
||||
i, _ := strconv.Atoi(s)
|
||||
return i
|
||||
}
|
182
vendor/github.com/Azure/go-ansiterm/winterm/ansi.go
generated
vendored
Normal file
182
vendor/github.com/Azure/go-ansiterm/winterm/ansi.go
generated
vendored
Normal file
@ -0,0 +1,182 @@
|
||||
// +build windows
|
||||
|
||||
package winterm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/Azure/go-ansiterm"
|
||||
)
|
||||
|
||||
// Windows keyboard constants
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx.
|
||||
const (
|
||||
VK_PRIOR = 0x21 // PAGE UP key
|
||||
VK_NEXT = 0x22 // PAGE DOWN key
|
||||
VK_END = 0x23 // END key
|
||||
VK_HOME = 0x24 // HOME key
|
||||
VK_LEFT = 0x25 // LEFT ARROW key
|
||||
VK_UP = 0x26 // UP ARROW key
|
||||
VK_RIGHT = 0x27 // RIGHT ARROW key
|
||||
VK_DOWN = 0x28 // DOWN ARROW key
|
||||
VK_SELECT = 0x29 // SELECT key
|
||||
VK_PRINT = 0x2A // PRINT key
|
||||
VK_EXECUTE = 0x2B // EXECUTE key
|
||||
VK_SNAPSHOT = 0x2C // PRINT SCREEN key
|
||||
VK_INSERT = 0x2D // INS key
|
||||
VK_DELETE = 0x2E // DEL key
|
||||
VK_HELP = 0x2F // HELP key
|
||||
VK_F1 = 0x70 // F1 key
|
||||
VK_F2 = 0x71 // F2 key
|
||||
VK_F3 = 0x72 // F3 key
|
||||
VK_F4 = 0x73 // F4 key
|
||||
VK_F5 = 0x74 // F5 key
|
||||
VK_F6 = 0x75 // F6 key
|
||||
VK_F7 = 0x76 // F7 key
|
||||
VK_F8 = 0x77 // F8 key
|
||||
VK_F9 = 0x78 // F9 key
|
||||
VK_F10 = 0x79 // F10 key
|
||||
VK_F11 = 0x7A // F11 key
|
||||
VK_F12 = 0x7B // F12 key
|
||||
|
||||
RIGHT_ALT_PRESSED = 0x0001
|
||||
LEFT_ALT_PRESSED = 0x0002
|
||||
RIGHT_CTRL_PRESSED = 0x0004
|
||||
LEFT_CTRL_PRESSED = 0x0008
|
||||
SHIFT_PRESSED = 0x0010
|
||||
NUMLOCK_ON = 0x0020
|
||||
SCROLLLOCK_ON = 0x0040
|
||||
CAPSLOCK_ON = 0x0080
|
||||
ENHANCED_KEY = 0x0100
|
||||
)
|
||||
|
||||
type ansiCommand struct {
|
||||
CommandBytes []byte
|
||||
Command string
|
||||
Parameters []string
|
||||
IsSpecial bool
|
||||
}
|
||||
|
||||
func newAnsiCommand(command []byte) *ansiCommand {
|
||||
|
||||
if isCharacterSelectionCmdChar(command[1]) {
|
||||
// Is Character Set Selection commands
|
||||
return &ansiCommand{
|
||||
CommandBytes: command,
|
||||
Command: string(command),
|
||||
IsSpecial: true,
|
||||
}
|
||||
}
|
||||
|
||||
// last char is command character
|
||||
lastCharIndex := len(command) - 1
|
||||
|
||||
ac := &ansiCommand{
|
||||
CommandBytes: command,
|
||||
Command: string(command[lastCharIndex]),
|
||||
IsSpecial: false,
|
||||
}
|
||||
|
||||
// more than a single escape
|
||||
if lastCharIndex != 0 {
|
||||
start := 1
|
||||
// skip if double char escape sequence
|
||||
if command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_ESCAPE_SECONDARY {
|
||||
start++
|
||||
}
|
||||
// convert this to GetNextParam method
|
||||
ac.Parameters = strings.Split(string(command[start:lastCharIndex]), ansiterm.ANSI_PARAMETER_SEP)
|
||||
}
|
||||
|
||||
return ac
|
||||
}
|
||||
|
||||
func (ac *ansiCommand) paramAsSHORT(index int, defaultValue int16) int16 {
|
||||
if index < 0 || index >= len(ac.Parameters) {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
param, err := strconv.ParseInt(ac.Parameters[index], 10, 16)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return int16(param)
|
||||
}
|
||||
|
||||
func (ac *ansiCommand) String() string {
|
||||
return fmt.Sprintf("0x%v \"%v\" (\"%v\")",
|
||||
bytesToHex(ac.CommandBytes),
|
||||
ac.Command,
|
||||
strings.Join(ac.Parameters, "\",\""))
|
||||
}
|
||||
|
||||
// isAnsiCommandChar returns true if the passed byte falls within the range of ANSI commands.
|
||||
// See http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html.
|
||||
func isAnsiCommandChar(b byte) bool {
|
||||
switch {
|
||||
case ansiterm.ANSI_COMMAND_FIRST <= b && b <= ansiterm.ANSI_COMMAND_LAST && b != ansiterm.ANSI_ESCAPE_SECONDARY:
|
||||
return true
|
||||
case b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_OSC || b == ansiterm.ANSI_CMD_DECPAM || b == ansiterm.ANSI_CMD_DECPNM:
|
||||
// non-CSI escape sequence terminator
|
||||
return true
|
||||
case b == ansiterm.ANSI_CMD_STR_TERM || b == ansiterm.ANSI_BEL:
|
||||
// String escape sequence terminator
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isXtermOscSequence(command []byte, current byte) bool {
|
||||
return (len(command) >= 2 && command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_CMD_OSC && current != ansiterm.ANSI_BEL)
|
||||
}
|
||||
|
||||
func isCharacterSelectionCmdChar(b byte) bool {
|
||||
return (b == ansiterm.ANSI_CMD_G0 || b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_G2 || b == ansiterm.ANSI_CMD_G3)
|
||||
}
|
||||
|
||||
// bytesToHex converts a slice of bytes to a human-readable string.
|
||||
func bytesToHex(b []byte) string {
|
||||
hex := make([]string, len(b))
|
||||
for i, ch := range b {
|
||||
hex[i] = fmt.Sprintf("%X", ch)
|
||||
}
|
||||
return strings.Join(hex, "")
|
||||
}
|
||||
|
||||
// ensureInRange adjusts the passed value, if necessary, to ensure it is within
|
||||
// the passed min / max range.
|
||||
func ensureInRange(n int16, min int16, max int16) int16 {
|
||||
if n < min {
|
||||
return min
|
||||
} else if n > max {
|
||||
return max
|
||||
} else {
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
func GetStdFile(nFile int) (*os.File, uintptr) {
|
||||
var file *os.File
|
||||
switch nFile {
|
||||
case syscall.STD_INPUT_HANDLE:
|
||||
file = os.Stdin
|
||||
case syscall.STD_OUTPUT_HANDLE:
|
||||
file = os.Stdout
|
||||
case syscall.STD_ERROR_HANDLE:
|
||||
file = os.Stderr
|
||||
default:
|
||||
panic(fmt.Errorf("Invalid standard handle identifier: %v", nFile))
|
||||
}
|
||||
|
||||
fd, err := syscall.GetStdHandle(nFile)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Invalid standard handle identifier: %v -- %v", nFile, err))
|
||||
}
|
||||
|
||||
return file, uintptr(fd)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user