From 5565ea8fe68f3b9e1818a2a1bbcf050a32f32ed9 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 6 Nov 2023 16:31:09 -0800 Subject: [PATCH 1/5] Restructure directories --- go.mod => lib/mage/go.mod | 0 go.sum => lib/mage/go.sum | 0 mageerrors.go => lib/mage/mageerrors.go | 0 magefile.go => lib/mage/magefile.go | 0 {docker => samples}/example/Dockerfile | 0 {tools => samples}/example/cmd/main.go | 0 samples/example/{ => configs}/config.yaml | 0 {tools => samples}/example/pkg/example/example.go | 0 {tools => samples}/example/pkg/example/example_test.go | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename go.mod => lib/mage/go.mod (100%) rename go.sum => lib/mage/go.sum (100%) rename mageerrors.go => lib/mage/mageerrors.go (100%) rename magefile.go => lib/mage/magefile.go (100%) rename {docker => samples}/example/Dockerfile (100%) rename {tools => samples}/example/cmd/main.go (100%) rename samples/example/{ => configs}/config.yaml (100%) rename {tools => samples}/example/pkg/example/example.go (100%) rename {tools => samples}/example/pkg/example/example_test.go (100%) diff --git a/go.mod b/lib/mage/go.mod similarity index 100% rename from go.mod rename to lib/mage/go.mod diff --git a/go.sum b/lib/mage/go.sum similarity index 100% rename from go.sum rename to lib/mage/go.sum diff --git a/mageerrors.go b/lib/mage/mageerrors.go similarity index 100% rename from mageerrors.go rename to lib/mage/mageerrors.go diff --git a/magefile.go b/lib/mage/magefile.go similarity index 100% rename from magefile.go rename to lib/mage/magefile.go diff --git a/docker/example/Dockerfile b/samples/example/Dockerfile similarity index 100% rename from docker/example/Dockerfile rename to samples/example/Dockerfile diff --git a/tools/example/cmd/main.go b/samples/example/cmd/main.go similarity index 100% rename from tools/example/cmd/main.go rename to samples/example/cmd/main.go diff --git a/samples/example/config.yaml b/samples/example/configs/config.yaml similarity index 100% rename from samples/example/config.yaml rename to samples/example/configs/config.yaml diff --git a/tools/example/pkg/example/example.go b/samples/example/pkg/example/example.go similarity index 100% rename from tools/example/pkg/example/example.go rename to samples/example/pkg/example/example.go diff --git a/tools/example/pkg/example/example_test.go b/samples/example/pkg/example/example_test.go similarity index 100% rename from tools/example/pkg/example/example_test.go rename to samples/example/pkg/example/example_test.go From 163b59f08bfca327d05ece1d39d5ca2cd8b0a375 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 6 Nov 2023 16:56:33 -0800 Subject: [PATCH 2/5] Restructuring --- lib/mage/go.mod | 2 +- samples/example/go.mod | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 samples/example/go.mod diff --git a/lib/mage/go.mod b/lib/mage/go.mod index c0523c8..75a01ea 100644 --- a/lib/mage/go.mod +++ b/lib/mage/go.mod @@ -1,4 +1,4 @@ -module github.com/Azure-Samples/aio-dev-toolbox/toolbox +module github.com/Azure-Samples/aio-dev-toolbox/lib/mage go 1.21.3 diff --git a/samples/example/go.mod b/samples/example/go.mod new file mode 100644 index 0000000..96332e1 --- /dev/null +++ b/samples/example/go.mod @@ -0,0 +1,3 @@ +module github.com/Azure-Samples/aio-dev-toolbox/samples/example + +go 1.21.3 \ No newline at end of file From 6d7fb954d92ffc62bbcba2122e87b943087e2018 Mon Sep 17 00:00:00 2001 From: Ethan Date: Tue, 7 Nov 2023 13:28:10 -0800 Subject: [PATCH 3/5] Add krill project to repo --- .gitignore | 4 - lib/example/example.go | 11 - lib/example/example_test.go | 8 - lib/mage/go.mod | 2 +- samples/example/Dockerfile | 9 - samples/example/cmd/main.go | 10 - samples/example/configs/config.yaml | 2 - samples/example/go.mod | 3 - samples/example/pkg/example/example.go | 11 - samples/example/pkg/example/example_test.go | 8 - samples/krill/.gitignore | 16 + samples/krill/.golangci.yml | 36 ++ samples/krill/Dockerfile | 17 + samples/krill/README.md | 181 ++++++ samples/krill/cmd/krill/main.go | 201 +++++++ samples/krill/components/broker/broker.go | 59 ++ .../krill/components/broker/broker_test.go | 30 + samples/krill/components/broker/service.go | 48 ++ .../krill/components/broker/service_test.go | 74 +++ samples/krill/components/client/client.go | 356 ++++++++++++ .../krill/components/client/client_test.go | 533 ++++++++++++++++++ samples/krill/components/client/clientv5.go | 261 +++++++++ samples/krill/components/client/errors.go | 32 ++ samples/krill/components/client/service.go | 146 +++++ .../krill/components/client/service_test.go | 397 +++++++++++++ samples/krill/components/edge/edge_test.go | 202 +++++++ samples/krill/components/edge/errors.go | 61 ++ samples/krill/components/edge/service.go | 97 ++++ samples/krill/components/formatter/errors.go | 21 + .../krill/components/formatter/formatter.go | 198 +++++++ .../components/formatter/formatter_test.go | 170 ++++++ samples/krill/components/formatter/service.go | 63 +++ .../components/formatter/service_test.go | 111 ++++ samples/krill/components/limiter/errors.go | 25 + samples/krill/components/limiter/limiter.go | 121 ++++ .../krill/components/limiter/limiter_test.go | 80 +++ samples/krill/components/limiter/service.go | 53 ++ .../krill/components/limiter/service_test.go | 67 +++ samples/krill/components/node/errors.go | 33 ++ samples/krill/components/node/node_test.go | 101 ++++ samples/krill/components/node/service.go | 74 +++ samples/krill/components/observer/observer.go | 56 ++ .../components/observer/observer_test.go | 78 +++ samples/krill/components/observer/service.go | 52 ++ .../krill/components/observer/service_test.go | 116 ++++ samples/krill/components/outlet/outlet.go | 70 +++ .../krill/components/outlet/outlet_test.go | 85 +++ samples/krill/components/outlet/service.go | 62 ++ .../krill/components/outlet/service_test.go | 92 +++ samples/krill/components/provider/errors.go | 21 + samples/krill/components/provider/provider.go | 47 ++ .../components/provider/provider_test.go | 31 + samples/krill/components/provider/service.go | 110 ++++ .../krill/components/provider/service_test.go | 192 +++++++ .../krill/components/publisher/publisher.go | 148 +++++ .../components/publisher/publisher_test.go | 156 +++++ samples/krill/components/publisher/service.go | 140 +++++ .../components/publisher/service_test.go | 228 ++++++++ samples/krill/components/registry/registry.go | 107 ++++ .../components/registry/registry_test.go | 58 ++ samples/krill/components/registry/service.go | 28 + .../krill/components/registry/service_test.go | 27 + samples/krill/components/renderer/renderer.go | 40 ++ .../components/renderer/renderer_test.go | 47 ++ samples/krill/components/renderer/service.go | 47 ++ .../krill/components/renderer/service_test.go | 79 +++ samples/krill/components/site/service.go | 46 ++ samples/krill/components/site/service_test.go | 74 +++ samples/krill/components/site/site.go | 43 ++ samples/krill/components/site/site_test.go | 17 + .../krill/components/subscriber/service.go | 116 ++++ .../components/subscriber/service_test.go | 267 +++++++++ .../krill/components/subscriber/subscriber.go | 115 ++++ .../components/subscriber/subscriber_test.go | 152 +++++ samples/krill/components/topic/service.go | 46 ++ .../krill/components/topic/service_test.go | 74 +++ samples/krill/components/topic/topic.go | 38 ++ samples/krill/components/topic/topic_test.go | 25 + samples/krill/components/tracer/service.go | 54 ++ .../krill/components/tracer/service_test.go | 76 +++ samples/krill/components/tracer/tracer.go | 71 +++ .../krill/components/tracer/tracer_test.go | 46 ++ samples/krill/configs/simple/config.yml | 22 + samples/krill/go.mod | 51 ++ samples/krill/go.sum | 207 +++++++ samples/krill/lib/binary/binary.go | 64 +++ samples/krill/lib/binary/binary_test.go | 77 +++ samples/krill/lib/component/component.go | 19 + samples/krill/lib/component/component_test.go | 73 +++ samples/krill/lib/component/errors.go | 7 + samples/krill/lib/component/mock.go | 46 ++ samples/krill/lib/component/store.go | 77 +++ samples/krill/lib/component/store_test.go | 71 +++ samples/krill/lib/composition/composition.go | 179 ++++++ .../krill/lib/composition/composition_test.go | 128 +++++ samples/krill/lib/composition/mock.go | 27 + samples/krill/lib/counter/counter.go | 145 +++++ samples/krill/lib/counter/counter_test.go | 104 ++++ samples/krill/lib/dialer/dialer.go | 147 +++++ samples/krill/lib/dialer/dialer_test.go | 121 ++++ samples/krill/lib/env/env.go | 98 ++++ samples/krill/lib/env/env_test.go | 166 ++++++ samples/krill/lib/env/errors.go | 25 + samples/krill/lib/environment/environment.go | 46 ++ .../krill/lib/environment/environment_test.go | 30 + samples/krill/lib/errors/errors.go | 49 ++ samples/krill/lib/errors/errors_test.go | 143 +++++ samples/krill/lib/errors/fiber.go | 90 +++ samples/krill/lib/exporter/exporter.go | 153 +++++ samples/krill/lib/exporter/exporter_test.go | 236 ++++++++ samples/krill/lib/exporter/mock.go | 49 ++ samples/krill/lib/exporter/provider.go | 50 ++ samples/krill/lib/exporter/stat.go | 29 + samples/krill/lib/expression/expression.go | 465 +++++++++++++++ .../krill/lib/expression/expression_test.go | 200 +++++++ samples/krill/lib/flatten/flatten.go | 103 ++++ samples/krill/lib/flatten/flatten_test.go | 88 +++ samples/krill/lib/gauge/gauge.go | 129 +++++ samples/krill/lib/gauge/gauge_test.go | 104 ++++ samples/krill/lib/histogram/histogram.go | 170 ++++++ samples/krill/lib/histogram/histogram_test.go | 110 ++++ samples/krill/lib/krill/configuration.go | 138 +++++ samples/krill/lib/krill/krill.go | 470 +++++++++++++++ samples/krill/lib/krill/krill_test.go | 448 +++++++++++++++ samples/krill/lib/logger/logger.go | 210 +++++++ samples/krill/lib/logger/logger_test.go | 44 ++ samples/krill/lib/proto/gen.sh | 3 + samples/krill/lib/proto/message.pb.go | 246 ++++++++ samples/krill/lib/proto/message.proto | 13 + samples/krill/lib/proto/proto.go | 100 ++++ samples/krill/lib/proto/proto_test.go | 36 ++ samples/krill/lib/templater/templater.go | 85 +++ samples/krill/lib/templater/templater_test.go | 105 ++++ 133 files changed, 13238 insertions(+), 67 deletions(-) delete mode 100644 lib/example/example.go delete mode 100644 lib/example/example_test.go delete mode 100644 samples/example/Dockerfile delete mode 100644 samples/example/cmd/main.go delete mode 100644 samples/example/configs/config.yaml delete mode 100644 samples/example/go.mod delete mode 100644 samples/example/pkg/example/example.go delete mode 100644 samples/example/pkg/example/example_test.go create mode 100644 samples/krill/.gitignore create mode 100644 samples/krill/.golangci.yml create mode 100644 samples/krill/Dockerfile create mode 100644 samples/krill/README.md create mode 100644 samples/krill/cmd/krill/main.go create mode 100644 samples/krill/components/broker/broker.go create mode 100644 samples/krill/components/broker/broker_test.go create mode 100644 samples/krill/components/broker/service.go create mode 100644 samples/krill/components/broker/service_test.go create mode 100644 samples/krill/components/client/client.go create mode 100644 samples/krill/components/client/client_test.go create mode 100644 samples/krill/components/client/clientv5.go create mode 100644 samples/krill/components/client/errors.go create mode 100644 samples/krill/components/client/service.go create mode 100644 samples/krill/components/client/service_test.go create mode 100644 samples/krill/components/edge/edge_test.go create mode 100644 samples/krill/components/edge/errors.go create mode 100644 samples/krill/components/edge/service.go create mode 100644 samples/krill/components/formatter/errors.go create mode 100644 samples/krill/components/formatter/formatter.go create mode 100644 samples/krill/components/formatter/formatter_test.go create mode 100644 samples/krill/components/formatter/service.go create mode 100644 samples/krill/components/formatter/service_test.go create mode 100644 samples/krill/components/limiter/errors.go create mode 100755 samples/krill/components/limiter/limiter.go create mode 100755 samples/krill/components/limiter/limiter_test.go create mode 100644 samples/krill/components/limiter/service.go create mode 100644 samples/krill/components/limiter/service_test.go create mode 100644 samples/krill/components/node/errors.go create mode 100644 samples/krill/components/node/node_test.go create mode 100644 samples/krill/components/node/service.go create mode 100644 samples/krill/components/observer/observer.go create mode 100644 samples/krill/components/observer/observer_test.go create mode 100644 samples/krill/components/observer/service.go create mode 100644 samples/krill/components/observer/service_test.go create mode 100644 samples/krill/components/outlet/outlet.go create mode 100644 samples/krill/components/outlet/outlet_test.go create mode 100644 samples/krill/components/outlet/service.go create mode 100644 samples/krill/components/outlet/service_test.go create mode 100644 samples/krill/components/provider/errors.go create mode 100644 samples/krill/components/provider/provider.go create mode 100644 samples/krill/components/provider/provider_test.go create mode 100644 samples/krill/components/provider/service.go create mode 100644 samples/krill/components/provider/service_test.go create mode 100644 samples/krill/components/publisher/publisher.go create mode 100644 samples/krill/components/publisher/publisher_test.go create mode 100644 samples/krill/components/publisher/service.go create mode 100644 samples/krill/components/publisher/service_test.go create mode 100644 samples/krill/components/registry/registry.go create mode 100644 samples/krill/components/registry/registry_test.go create mode 100644 samples/krill/components/registry/service.go create mode 100644 samples/krill/components/registry/service_test.go create mode 100644 samples/krill/components/renderer/renderer.go create mode 100644 samples/krill/components/renderer/renderer_test.go create mode 100644 samples/krill/components/renderer/service.go create mode 100644 samples/krill/components/renderer/service_test.go create mode 100644 samples/krill/components/site/service.go create mode 100644 samples/krill/components/site/service_test.go create mode 100644 samples/krill/components/site/site.go create mode 100644 samples/krill/components/site/site_test.go create mode 100644 samples/krill/components/subscriber/service.go create mode 100644 samples/krill/components/subscriber/service_test.go create mode 100644 samples/krill/components/subscriber/subscriber.go create mode 100644 samples/krill/components/subscriber/subscriber_test.go create mode 100644 samples/krill/components/topic/service.go create mode 100644 samples/krill/components/topic/service_test.go create mode 100644 samples/krill/components/topic/topic.go create mode 100644 samples/krill/components/topic/topic_test.go create mode 100644 samples/krill/components/tracer/service.go create mode 100644 samples/krill/components/tracer/service_test.go create mode 100644 samples/krill/components/tracer/tracer.go create mode 100644 samples/krill/components/tracer/tracer_test.go create mode 100644 samples/krill/configs/simple/config.yml create mode 100644 samples/krill/go.mod create mode 100644 samples/krill/go.sum create mode 100644 samples/krill/lib/binary/binary.go create mode 100644 samples/krill/lib/binary/binary_test.go create mode 100644 samples/krill/lib/component/component.go create mode 100644 samples/krill/lib/component/component_test.go create mode 100644 samples/krill/lib/component/errors.go create mode 100644 samples/krill/lib/component/mock.go create mode 100644 samples/krill/lib/component/store.go create mode 100644 samples/krill/lib/component/store_test.go create mode 100644 samples/krill/lib/composition/composition.go create mode 100644 samples/krill/lib/composition/composition_test.go create mode 100644 samples/krill/lib/composition/mock.go create mode 100644 samples/krill/lib/counter/counter.go create mode 100644 samples/krill/lib/counter/counter_test.go create mode 100644 samples/krill/lib/dialer/dialer.go create mode 100644 samples/krill/lib/dialer/dialer_test.go create mode 100644 samples/krill/lib/env/env.go create mode 100644 samples/krill/lib/env/env_test.go create mode 100644 samples/krill/lib/env/errors.go create mode 100644 samples/krill/lib/environment/environment.go create mode 100644 samples/krill/lib/environment/environment_test.go create mode 100644 samples/krill/lib/errors/errors.go create mode 100644 samples/krill/lib/errors/errors_test.go create mode 100644 samples/krill/lib/errors/fiber.go create mode 100644 samples/krill/lib/exporter/exporter.go create mode 100644 samples/krill/lib/exporter/exporter_test.go create mode 100644 samples/krill/lib/exporter/mock.go create mode 100644 samples/krill/lib/exporter/provider.go create mode 100644 samples/krill/lib/exporter/stat.go create mode 100644 samples/krill/lib/expression/expression.go create mode 100644 samples/krill/lib/expression/expression_test.go create mode 100644 samples/krill/lib/flatten/flatten.go create mode 100644 samples/krill/lib/flatten/flatten_test.go create mode 100644 samples/krill/lib/gauge/gauge.go create mode 100644 samples/krill/lib/gauge/gauge_test.go create mode 100644 samples/krill/lib/histogram/histogram.go create mode 100644 samples/krill/lib/histogram/histogram_test.go create mode 100644 samples/krill/lib/krill/configuration.go create mode 100644 samples/krill/lib/krill/krill.go create mode 100644 samples/krill/lib/krill/krill_test.go create mode 100644 samples/krill/lib/logger/logger.go create mode 100644 samples/krill/lib/logger/logger_test.go create mode 100755 samples/krill/lib/proto/gen.sh create mode 100644 samples/krill/lib/proto/message.pb.go create mode 100644 samples/krill/lib/proto/message.proto create mode 100644 samples/krill/lib/proto/proto.go create mode 100644 samples/krill/lib/proto/proto_test.go create mode 100755 samples/krill/lib/templater/templater.go create mode 100755 samples/krill/lib/templater/templater_test.go diff --git a/.gitignore b/.gitignore index 7bb8afa..8a30d25 100644 --- a/.gitignore +++ b/.gitignore @@ -396,7 +396,3 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml - -bin -cover.tmp.out -coverage.out \ No newline at end of file diff --git a/lib/example/example.go b/lib/example/example.go deleted file mode 100644 index 6c31d3e..0000000 --- a/lib/example/example.go +++ /dev/null @@ -1,11 +0,0 @@ -package example - -import "fmt" - -// Example shows that all exported symbols must have a comment like this. -type Example struct {} - -// Print shows that the exported symbol comments applies to functions as well. -func (*Example) Print() { - fmt.Println("Example library") -} \ No newline at end of file diff --git a/lib/example/example_test.go b/lib/example/example_test.go deleted file mode 100644 index 109a6a8..0000000 --- a/lib/example/example_test.go +++ /dev/null @@ -1,8 +0,0 @@ -package example - -import "testing" - -// Every library must have a test file and must meet a minimum test coverage to be merged into the toolbox. -func TestMain(m *testing.M) { - m.Run() -} diff --git a/lib/mage/go.mod b/lib/mage/go.mod index 75a01ea..f295d21 100644 --- a/lib/mage/go.mod +++ b/lib/mage/go.mod @@ -1,4 +1,4 @@ -module github.com/Azure-Samples/aio-dev-toolbox/lib/mage +module github.com/Azure-Samples/explore-iot-operations/lib/mage go 1.21.3 diff --git a/samples/example/Dockerfile b/samples/example/Dockerfile deleted file mode 100644 index 05e26a0..0000000 --- a/samples/example/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -# syntax=docker/dockerfile:1 - -FROM mcr.microsoft.com/oss/go/microsoft/golang:1.21-cbl-mariner2.0 - -COPY go.mod /tool/go.mod -COPY go.sum /tool/go.sum -COPY lib /tool/lib -COPY tools/example /tool/example - diff --git a/samples/example/cmd/main.go b/samples/example/cmd/main.go deleted file mode 100644 index 99d1b49..0000000 --- a/samples/example/cmd/main.go +++ /dev/null @@ -1,10 +0,0 @@ -package main - -import ( - "github.com/Azure-Samples/aio-dev-toolbox/toolbox/tools/example/pkg/example" -) - -func main() { - example := &example.Example{} - example.Print() -} diff --git a/samples/example/configs/config.yaml b/samples/example/configs/config.yaml deleted file mode 100644 index afb8cf7..0000000 --- a/samples/example/configs/config.yaml +++ /dev/null @@ -1,2 +0,0 @@ -# This is an example configuration which would be used with the example project. -# Multiple configuration files can live within the samples/example directory. \ No newline at end of file diff --git a/samples/example/go.mod b/samples/example/go.mod deleted file mode 100644 index 96332e1..0000000 --- a/samples/example/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/Azure-Samples/aio-dev-toolbox/samples/example - -go 1.21.3 \ No newline at end of file diff --git a/samples/example/pkg/example/example.go b/samples/example/pkg/example/example.go deleted file mode 100644 index b81f9a3..0000000 --- a/samples/example/pkg/example/example.go +++ /dev/null @@ -1,11 +0,0 @@ -package example - -import "fmt" - -// Example shows that all exported symbols must have a comment like this. -type Example struct {} - -// Print shows that the exported symbol comments applies to functions as well. -func (*Example) Print() { - fmt.Println("Example package") -} \ No newline at end of file diff --git a/samples/example/pkg/example/example_test.go b/samples/example/pkg/example/example_test.go deleted file mode 100644 index e62fd50..0000000 --- a/samples/example/pkg/example/example_test.go +++ /dev/null @@ -1,8 +0,0 @@ -package example - -import "testing" - -// Every package must have a test file and must meet a minimum test coverage to be merged into the toolbox. -func TestMain(m *testing.M) { - m.Run() -} diff --git a/samples/krill/.gitignore b/samples/krill/.gitignore new file mode 100644 index 0000000..5e86a38 --- /dev/null +++ b/samples/krill/.gitignore @@ -0,0 +1,16 @@ +bin +.vscode/* +/mako +dns.yaml +/src/**/config.json +/bin +/krill +/listen +/mqtt +/manager +/simulator +dist/ +cover.tmp.out +coverage.out +web +cover.* \ No newline at end of file diff --git a/samples/krill/.golangci.yml b/samples/krill/.golangci.yml new file mode 100644 index 0000000..9a93431 --- /dev/null +++ b/samples/krill/.golangci.yml @@ -0,0 +1,36 @@ +linters: + enable: + - gofmt + - gocritic + - gosec + - govet + - prealloc + - unconvert + +linters-settings: + gocritic: + enabled-tags: + - performance + - style + - experimental + + disabled-checks: + - redundantSprint + - hugeParam + - rangeValCopy + - typeDefFirst + - paramTypeCombine + - externalErrorReassign + +issues: + exclude: + # errcheck: Configure linter to disregard unchecked returned errors for these functions. + - Error return value of + .((os\.)?std(out|err)\..*|.*Close|.*LockRelease|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv|.*Err). + is not checked + - unnamedResult + + exclude-use-default: false + +run: + go: "1.21" \ No newline at end of file diff --git a/samples/krill/Dockerfile b/samples/krill/Dockerfile new file mode 100644 index 0000000..3f45cb8 --- /dev/null +++ b/samples/krill/Dockerfile @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:1 + +FROM mcr.microsoft.com/oss/go/microsoft/golang:1.21-cbl-mariner2.0 AS build + +COPY . /app + +WORKDIR /app + +RUN go mod download + +RUN go fmt ./... + +RUN go build -o ./bin/krill ./cmd/krill + +EXPOSE 2112 + +CMD [ "/bin/krill" ] diff --git a/samples/krill/README.md b/samples/krill/README.md new file mode 100644 index 0000000..0a34b7c --- /dev/null +++ b/samples/krill/README.md @@ -0,0 +1,181 @@ +# Krill + +``` +⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣤⣤⣀⠀⠀⠀⠀⠀⠀ +⠀⠀⢀⣀⡙⠻⢶⣶⣦⣴⣶⣶⣶⠾⠛⠛⠋⠉⠉⠉⠉⠙⠃⠀⠀⠀⠀⠀ +⠀⠀⠀⠉⠉⠙⠛⠛⠋⠉⠉⠡⣤⣴⣶⣶⣾⣿⣿⣿⣛⣩⣤⡤⠖⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⢠⣴⣾⠂⣴⣦⠈⣿⣿⣿⣿⣿⣿⠿⠛⣋⠁⠀⠀⠀⠀⠀ +⠀⠀⢀⣼⣿⣶⣄⡉⠻⣧⣌⣁⣴⣿⣿⣿⣿⣿⣿⡿⠛⠁⠀⠀⠀⠀⠀⠀ +⠀⠀⣾⣿⣿⣿⣿⣿⣦⡈⢻⣿⣿⣿⣿⡿⠿⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⡀⢻⣿⣿⣿⣿⣿⣿⣿⡄⠙⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⢠⣷⣄⡉⠻⢿⣿⣿⣿⠏⠠⢶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⢸⣿⣿⣿⣶⣤⣈⠙⠁⠰⣦⣀⠉⠻⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠘⢿⣿⣿⣿⣿⣿⡇⠠⣦⣄⠉⠳⣤⠈⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⢠⣌⣉⡉⠉⣉⡁⠀⠀⠙⠗⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠹⢿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠙⠻⣿⣿⠟⢀⣤⡀⠀⠀⠀⠀⠀⠀⣀⣀⣠⣤⣤⣤⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠿⠿⡿⠂⣀⣠⣤⣤⣤⣀⣉⣉⠉⠉⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠙⠛⠛⠛⠛⠋⠉⠉⠁ +``` + +Krill is a command line utility for simulating MQTT assets. + +## Usage + +`./krill [--config config_file_path] [--yaml yaml_format_bool]` + +Config will be supplied with the default value of "config.yml", and the yaml boolean will be set to true. If the yaml boolean is set to false, krill will attempt to decode using JSON instead of YAML. + +See the example YAML configuration below, with comments describing the various configurable fields. + +``` +ports: + metrics: 2113 # Port to host prometheus formatted metrics. + refData: 2114 # Application server port for ref data. +logLevel: 5 # Log level (debug: 0, info: 1, warn: 2, error: 3, critical: 4, fatal: 5, panic: 6) +simulation: + target: # Target broker information. + endpoint: localhost + port: 1883 + sites: # List of sites, a container for one or more assets. + - name: site2 # Name of the site. + assetCount: 1 # Number of assets in this site. + tags: # List of tags this asset will send. + - id: float_0 # ID of this tag (must be unique). + configuration: sin(float(x)) * 10.0 # Configuration of this tag. + count: 1 # Number of copies of this tag. + - id: float_1 + configuration: abs(sin(float(x) / 3.0) * 10.0) + count: 1 + - id: string_0 + configuration: '"constant"' + count: 1 + - id: string_1 + configuration: randstr(10) + count: 1 + - id: datetime_0 + configuration: now() + count: 1 + - id: datetime_1 + configuration: after(start, delta(now(), start) + 500) + count: 1 + - id: int_0 + configuration: delta(now(), start) + count: 1 + - id: int_1 + configuration: rand(0, 100) + count: 1 + - id: indexing_0 + configuration: p.site2__int_1__0 + count: 1 + - id: indexing_1 + configuration: p.site2__int_2__0 + count: 1 + - id: square_wave + configuration: sin(2.0 * sin(2.0 * sin(sin(2.0 * sin(sin(2.0 * sin(float(x / 2)))))))) + count: 1 + - id: saw_tooth_wave + configuration: sin(float(x)) - (1.0 / 2.0) * sin(float(x * 2)) + (1.0 / 3.0) * sin(float(x * 3)) - (1.0 / 4.0) * sin(float(x * 4)) + count: 1 + rate: # Rate of messages sent per period of seconds (limit / periodSeconds). + limit: 2 + messagesPerPeriod: 1 + periodSeconds: 1 + tagsPerMessage: 2 # Number of instances of a tag to render per unique tag. + payloadFormat: JSON # Format of message (JSON, JSONTagPerMessage, BigEndian, LittleEndian) + topicFormat: "{{.SiteName}}/{{.AssetName}}/{{.TagName}}" # Format of topic(s). + qosLevel: 1 # QoS level of published messages. + +``` + +### Configuring Payload Formats + +There are currently five supported types of payload formats: + +1. **JSON** - Sends all rendered tags as a collection of key value pairs in one JSON object. +1. **JSONTagPerMessage** - Sends each tag individually as a JSON object in its own message. +1. **BigEndian** - Sends all tags rendered as an array of bytes, with all numeric types formatted in a big endian format. +1. **LittleEndian** - Sends all tags rendered as an array of bytes, with all numeric types formatted in a little endian format. +1. **CSV** - CSV formatted message (will flatten any objects into CSV fields). +1. **Protobuf** - Protobuf encoded message. + +### Configuring Topics + +The following payload formats must only use the `{{.SiteName}}` and `{{.AssetName}}` template variables in formatting their topics: JSON, BigEndian, and LittleEndian. JSONTagPerMessage must use `{{.SiteName}}`, `{{.AssetName}}`, and `{{.TagName}}`, as each tag is sent in its own topic. SiteName will always be set to the name of the site as defined in the configuration. AssetName is a concatenation of SiteName and the asset ID in the form `__asset_`. TagName is a concatenation of SiteName, TagName, and the tag ID in the form `____`. + +As an example, `/site0/site0__asset_1/site0__my_tag__0` would be the topic used to publish the 1st asset's tag `my_tag` for `site0`, when using the JSONTagPerMessage format. + +### Configuring Equations + +Built-in functions: + +1. **sin** - _(x: float) → float_ , returns the sin of x. +1. **cos** - _(x: float) → float_ , returns the cos of x. +1. **tan** - _(x: float) → float_ , returns the tan of x. +1. **asin** - _(x: float) → float_ , returns the asin of x. +1. **acos** - _(x: float) → float_ , returns the acos of x. +1. **atan** - _(x: float) → float_ , returns the atan of x. +1. **rand** - _(x: int, y: int) → int_ , picks a random number between x and y, non-inclusive of y. +1. **str** - _(x: float, y: int) → int_ , converts x to a string representation, with the number of decimal places specified by y. +1. **concat** - _(x: string, y: string) → string_ , returns the concatenation of strings x and y. +1. **randstr** - _(x: int) → string_ , returns a string of random alphabetical characters of length x. +1. **now** - _() → datetime_ , returns the current time. +1. **delta** - _(x: datetime, y: datetime) → int_ , finds the delta between datetime x and y in terms of milliseconds. +1. **int** - _(x: float) → int_ , converts the float x into an int. +1. **float** - _(x: int) → float_ , converts the int x into a float. +1. **after** - _(x: datetime, y: int) → datetime_ , adds the number of milliseconds specified by y to the datetime x. +1. **abs** - _(x: float) → float_ , returns the absolute value of x. +1. **pi** - _() → float_ , returns the value of the constant pi. + +Symbols: + +1. **start** - set to the time at which the asset began publishing messages. +1. **x** - set to the message number, which starts at 0 and counts up. +1. **site** - set to the name of the site. +1. **id** - set to the id of this asset. +1. **p** - set to the structure of the previous sent message, and can be indexed into to obtain previously sent values. + +Valid Constants: + +1. **29** - integer values. +1. **29.1** - floating point values. +1. **"strings"** - string values. + +Operators: + +1. **float + float** +1. **float - float** +1. **float / float** +1. **float \* float** +1. **float ^ float** (power) +1. **int + int** +1. **int - int** +1. **int / int** +1. **int \* int** +1. **int ^ int** (power) +1. **int % int** (remainder) + +Example Equations: + +1. **after(start, delta(now(), start) + 500)** - returns a time 500 milliseconds after the current time. +1. **start** - returns the current time. +1. **sin(float(x)) - (1.0 / 2.0) _ sin(float(x _ 2)) + (1.0 / 3.0) _ sin(float(x _ 3)) - (1.0 / 4.0) _ sin(float(x _ 4))** - a sawtooth wave. +1. **p.site0**square_wave**0** - the value of the square tooth wave tag from the immediately preceding message. +1. **randstr(rand(1, 20))** - a random string of a random length between 1 and 20 characters. +1. **concat("message - ", str(float(x), 0))** - returns a string describing "message - \". + +### Other Notes + +1. If you enter an invalid equation, errors will be logged, but the value sent will be a constant 0. +2. If you attempt to divide 0 by 0, the result will be 0. +3. If you build an int too large, overflow will occur. +4. If you build a float too large, errors will be logged once the value reaches the infinity value or the NaN value. +5. Order of operations will be preserved, but parentheses are recommended. +6. If you return p as one of your return data values, you will recursively build a larger and larger structure until the message size will be too large to send to the MQTT broker. + +## Metrics + +Prometheus metrics are provided by the krill simulator at the port specified in the metrics field of the configuration. The available metrics are: + +1. `krill_entity_gauge` - shows the count of each system entity. +1. `krill__asset_publish_counter` - records the number of messages published, labeled by asset identifier. diff --git a/samples/krill/cmd/krill/main.go b/samples/krill/cmd/krill/main.go new file mode 100644 index 0000000..566b042 --- /dev/null +++ b/samples/krill/cmd/krill/main.go @@ -0,0 +1,201 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/iot-for-all/device-simulation/components/broker" + "github.com/iot-for-all/device-simulation/components/client" + "github.com/iot-for-all/device-simulation/components/edge" + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/components/limiter" + "github.com/iot-for-all/device-simulation/components/node" + "github.com/iot-for-all/device-simulation/components/observer" + "github.com/iot-for-all/device-simulation/components/outlet" + "github.com/iot-for-all/device-simulation/components/provider" + "github.com/iot-for-all/device-simulation/components/publisher" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/renderer" + "github.com/iot-for-all/device-simulation/components/site" + "github.com/iot-for-all/device-simulation/components/subscriber" + "github.com/iot-for-all/device-simulation/components/topic" + "github.com/iot-for-all/device-simulation/components/tracer" + "github.com/iot-for-all/device-simulation/lib/env" + "github.com/iot-for-all/device-simulation/lib/exporter" + "github.com/iot-for-all/device-simulation/lib/krill" + "github.com/iot-for-all/device-simulation/lib/logger" + "gopkg.in/yaml.v3" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog/log" +) + +func main() { + err := run() + if err != nil { + panic(err) + } +} + +func run() error { + + fmt.Print(krill.Krill) + + ctx := context.Background() + + reg := prometheus.NewRegistry() + + flagParser := env.NewFlagParser() + + flags, err := flagParser.ReadFlags(map[string]any{ + "config": "./config.yml", + "yaml": true, + "stdin": true, + }) + if err != nil { + return err + } + + unmarshal := yaml.Unmarshal + if !(*flags["yaml"].(*bool)) { + unmarshal = json.Unmarshal + } + + configReader := env.New[krill.Configuration](func(cr *env.ConfigurationReader[krill.Configuration]) { + cr.Unmarshal = unmarshal + if *flags["stdin"].(*bool) { + cr.ReadFile = func(_ string) ([]byte, error) { + return io.ReadAll(os.Stdin) + } + } + }) + + configuration, err := configReader.Read(*flags["config"].(*string)) + if err != nil { + return err + } + + lg := logger.NewZeroLoggerWrapper(log.Logger, func(zlw *logger.ZeroLoggerWrapper) { + zlw.LogLevel = configuration.LogLevel + }) + + lg.Printf("finished reading configuration") + + exp := &exporter.MockExporter{} + + lg.Printf("creating stores") + + brokerStore := broker.NewStore() + clientStore := client.NewStore() + edgeStore := edge.NewStore() + formatterStore := formatter.NewStore() + limiterStore := limiter.NewStore() + nodeStore := node.NewStore() + observerStore := observer.NewStore() + outletStore := outlet.NewStore() + providerStore := provider.NewStore() + publisherStore := publisher.NewStore() + registryStore := registry.NewStore() + rendererStore := renderer.NewStore() + siteStore := site.NewStore() + subscriberStore := subscriber.NewStore() + topicStore := topic.NewStore() + tracerStore := tracer.NewStore() + + lg.Printf("creating services") + + svcTag := lg.Tag("service") + + brokerService := broker.NewService(brokerStore, registryStore) + clientService := client.NewService(ctx, clientStore, registryStore, brokerStore, siteStore, func(s *client.Service) { + s.Logger = svcTag.Tag("client") + }) + edgeService := edge.NewService(edgeStore, nodeStore) + formatterService := formatter.NewService(formatterStore) + limiterService := limiter.NewService(ctx, limiterStore) + nodeService := node.NewService(nodeStore, func(s *node.Service) { + s.Logger = svcTag.Tag("node") + }) + observerService := observer.NewService(observerStore, registryStore, providerStore) + outletService := outlet.NewService(outletStore, formatterStore, registryStore) + providerService := provider.NewService(providerStore, reg, exp, func(s *provider.Service) { + s.Logger = svcTag.Tag("provider") + }) + publisherService := publisher.NewService(ctx, publisherStore, registryStore, clientStore, topicStore, rendererStore, limiterStore, tracerStore, func(s *publisher.Service) { + s.Logger = svcTag.Tag("publisher") + }) + registryService := registry.NewService(registryStore) + rendererService := renderer.NewService(rendererStore, formatterStore, nodeStore) + siteService := site.NewService(siteStore, registryStore) + subscriberService := subscriber.NewService(subscriberStore, clientStore, topicStore, outletStore, registryStore, tracerStore, func(s *subscriber.Service) { + s.Logger = svcTag.Tag("subscriber") + }) + topicService := topic.NewService(topicStore, registryStore) + tracerService := tracer.NewService(tracerStore, registryStore) + + builder := krill.New( + brokerService, + clientService, + edgeService, + formatterService, + limiterService, + nodeService, + observerService, + outletService, + providerService, + publisherService, + registryService, + rendererService, + siteService, + subscriberService, + topicService, + tracerService, + ) + + lg.Printf("parsing krill configuration") + + err = builder.Parse(configuration.Simulation) + if err != nil { + return err + } + + lg.Printf("setting up metrics server") + + // Set up prometheus servers. + promMetricsServer := &http.Server{ + ReadTimeout: 1 * time.Second, + WriteTimeout: 1 * time.Second, + IdleTimeout: 30 * time.Second, + ReadHeaderTimeout: 2 * time.Second, + Addr: fmt.Sprintf(":%d", configuration.Ports.Metrics), + } + + promCustomMetricsServerMux := http.NewServeMux() + promCustomMetricsServerMux.Handle( + "/metrics", + promhttp.HandlerFor( + reg, + promhttp.HandlerOpts{Registry: reg}, + ), + ) + + promMetricsServer.Handler = promCustomMetricsServerMux + + go func() { + <-ctx.Done() + err := promMetricsServer.Close() + if err != nil { + panic(err) + } + }() + + lg.Printf("finished setup") + + return promMetricsServer.ListenAndServe() +} diff --git a/samples/krill/components/broker/broker.go b/samples/krill/components/broker/broker.go new file mode 100644 index 0000000..01c8aea --- /dev/null +++ b/samples/krill/components/broker/broker.go @@ -0,0 +1,59 @@ +// Package broker provides the implementation of the broker component of the simulation framework. +package broker + +import ( + "fmt" + + "github.com/iot-for-all/device-simulation/components/registry" +) + +type Source interface { + Target + registry.Observable +} + +type Target interface { + Endpoint() string +} + +// Broker is a representation of an MQTT broker, containing that broker's endpoint and port. +// It also implements the Observable interface, allowing for monitoring based on other +// components which use this broker. +type Broker struct { + Broker string + Port int + endpoint string + registry.Observable +} + +// New creates a new broker, given an observable monitor. +// Optional parameters can be set through the options function. +func New(mon registry.Observable, options ...func(*Broker)) *Broker { + broker := &Broker{ + Observable: mon, + Broker: "", + Port: 0, + endpoint: "", + } + + for _, option := range options { + option(broker) + } + + broker.endpoint = fmt.Sprintf("%s:%d", broker.Broker, broker.Port) + + return broker +} + +func (broker *Broker) Endpoint() string { + return broker.endpoint +} + +type MockBroker struct { + registry.Observable + OnEndpoint func() string +} + +func (broker *MockBroker) Endpoint() string { + return broker.OnEndpoint() +} diff --git a/samples/krill/components/broker/broker_test.go b/samples/krill/components/broker/broker_test.go new file mode 100644 index 0000000..0f694a9 --- /dev/null +++ b/samples/krill/components/broker/broker_test.go @@ -0,0 +1,30 @@ +package broker + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockRegistryID = "MockRegistryID" + MockHost = "localhost" + MockPort = 4000 + MockEndpoint = "localhost:4000" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestCounterProviderWithInvalidName(t *testing.T) { + expected := MockEndpoint + + broker := New(nil, func(b *Broker) { + b.Broker = MockHost + b.Port = MockPort + }) + + require.Equal(t, expected, broker.Endpoint()) +} diff --git a/samples/krill/components/broker/service.go b/samples/krill/components/broker/service.go new file mode 100644 index 0000000..d8c1b1a --- /dev/null +++ b/samples/krill/components/broker/service.go @@ -0,0 +1,48 @@ +package broker + +import ( + "github.com/iot-for-all/device-simulation/components/observer" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" +) + +type Store component.Store[Source, component.ID] + +type Component struct { + RegistryID component.ID + Broker string + Port int +} + +type Service struct { + Store + registryStore registry.Store +} + +func NewStore() Store { + return component.New[Source, component.ID]() +} + +func NewService(store Store, registryStore registry.Store) *Service { + return &Service{ + Store: store, + registryStore: registryStore, + } +} + +func (service *Service) Create(id component.ID, c *Component) error { + var reg registry.Observable + reg, err := service.registryStore.Get(c.RegistryID) + if err != nil { + _, ok := err.(*component.NotFoundError) + if !ok { + return err + } + reg = &observer.NoopObservable{} + } + + return service.Store.Create(New(reg, func(b *Broker) { + b.Broker = c.Broker + b.Port = c.Port + }), id) +} diff --git a/samples/krill/components/broker/service_test.go b/samples/krill/components/broker/service_test.go new file mode 100644 index 0000000..3850d9d --- /dev/null +++ b/samples/krill/components/broker/service_test.go @@ -0,0 +1,74 @@ +package broker + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/observer" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/stretchr/testify/require" +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[Source, component.ID]) + require.True(t, ok) +} + +func TestService(t *testing.T) { + service := NewService(&component.MockStore[Source, component.ID]{ + OnCreate: func(entity Source, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + require.Equal(t, MockEndpoint, entity.Endpoint()) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return ®istry.MockRegistry{}, nil + }, + }) + + err := service.Create(MockID, &Component{ + RegistryID: MockRegistryID, + Broker: MockHost, + Port: MockPort, + }) + require.NoError(t, err) +} + +func TestOptionalRegistry(t *testing.T) { + service := NewService(&component.MockStore[Source, component.ID]{ + OnCreate: func(entity Source, identifier component.ID) error { + brkr, ok := entity.(*Broker) + require.True(t, ok) + require.Equal(t, &observer.NoopObservable{}, brkr.Observable) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.NotFoundError{} + }, + }) + + err := service.Create(MockID, &Component{}) + require.NoError(t, err) +} + +func TestRegistryError(t *testing.T) { + service := NewService(&component.MockStore[Source, component.ID]{ + OnCreate: func(entity Source, identifier component.ID) error { + brkr, ok := entity.(*Broker) + require.True(t, ok) + require.Equal(t, &observer.NoopObservable{}, brkr.Observable) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} diff --git a/samples/krill/components/client/client.go b/samples/krill/components/client/client.go new file mode 100644 index 0000000..28766d0 --- /dev/null +++ b/samples/krill/components/client/client.go @@ -0,0 +1,356 @@ +// Package client contains all MQTT client interfaces and implementations. +// It defines several client implementations, including MQTT v3 and v5 compatible clients +// as well as mocking clients for testing. +package client + +import ( + "context" + "fmt" + "time" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/site" + "github.com/iot-for-all/device-simulation/lib/logger" + + mqtt "github.com/eclipse/paho.mqtt.golang" +) + +// PublisherSubscriber is the composite interface which represents the capabilities of a full featured client. +// These include publishing, subscribing and unsubscribing, and connecting and disconnecting from the broker. +type PublisherSubscriber interface { + Publisher + Subscriber + BrokerConnection + site.Site + GetName() string +} + +// Publisher is an interface whose implementation should include the observable functionality (see registry package), +// as well as the ability to publish a message on a given topic. +type Publisher interface { + Publish(topic string, qos byte, messagesRetained bool, data []byte) error + ConnectionNotifier + registry.Observable +} + +// Subscriber is an interface whose implementation should be able to subscribe and unsubscribe from particular topics. +// It also includes the connection notifier functionality. +type Subscriber interface { + Subscribe(topic string, qos byte, onReceived func([]byte)) error + Unsubscribe(topic string) error + ConnectionNotifier +} + +// ConnectionNotifier is an interface whose implementation should be able to close a channel upon a successful +// MQTT broker connection and close another channel upon a successful broker disconnection. +type ConnectionNotifier interface { + Connected() chan struct{} + Disconnected() chan struct{} +} + +// BrokerConnection is an interface whose implementation should be able to connect and disconnect from an MQTT broker. +type BrokerConnection interface { + Connect() error + Disconnect() error +} + +// Client implements the ConnectionNotifier interface and serves as base functionality which can be included via +// composition in PublisherSubscriber implementations. +type Client struct { + ctx context.Context + onDisconnect chan struct{} + onConnect chan struct{} + registry.Observable + broker registry.Observable + site.Site + Name string + Logger logger.Logger + Debug logger.Logger + Trace logger.Logger +} + +// New creates a new Client, given a context. +func New( + ctx context.Context, + mon registry.Observable, + broker registry.Observable, + ste site.Site, + options ...func(*Client), +) *Client { + cli := &Client{ + onConnect: make(chan struct{}), + onDisconnect: make(chan struct{}), + broker: broker, + Site: ste, + Observable: mon, + ctx: ctx, + Logger: &logger.NoopLogger{}, + } + + for _, option := range options { + option(cli) + } + + cli.Debug = cli.Logger.Level(logger.Debug) + cli.Trace = cli.Logger.Level(logger.Trace) + + return cli +} + +// Connected returns the onConnect channel. +func (client *Client) Connected() chan struct{} { + return client.onConnect +} + +// Disconnected returns the onDisconnect channel. +func (client *Client) Disconnected() chan struct{} { + return client.onDisconnect +} + +func (client *Client) GetName() string { + return client.Name +} + +// Observe will pass a float64 value to be observed by the client monitor and the broker monitor. +func (client *Client) Observe(value float64) { + client.Observable.Observe(value) + client.broker.Observe(value) + client.Site.Observe(value) +} + +// Clientv3 is a full PublisherSubscriber implementation which follows the MQTTv3 client protocol. +type Clientv3 struct { + conn mqtt.Client + Client +} + +// NewClientv3 creates a Clientv3, given a paho mqttv3 connection and an underlying client. +func NewClientv3( + conn mqtt.Client, + cli Client, +) *Clientv3 { + return &Clientv3{ + conn: conn, + Client: cli, + } +} + +// Connect establishes a connection with an MQTT v3 compatible broker. +// It will block until the connection has succeeded or failed, and return an error if failed. +// It will also close the onConnect channel upon completion. +func (client *Clientv3) Connect() error { + client.Debug.Printf("attempting new connection with broker") + + token := client.conn.Connect() + + select { + case <-client.ctx.Done(): + client.Debug.Printf("connection to broker was interrupted by context cancellation") + return nil + case <-token.Done(): + } + + err := token.Error() + if err != nil { + client.Logger.Level(logger.Error).With("error", err.Error()).Printf("an error occurred when connection to the broker") + return err + } + + client.Debug.Printf("connection succeeded") + + close(client.onConnect) + + return nil +} + +// Disconnect will disconnect from a connected MQTT broker. +// It will also close the onDisconnect channel upon completion. +func (client *Clientv3) Disconnect() error { + client.Debug.Printf("attempting to disconnect from broker") + client.conn.Disconnect(0) + close(client.onDisconnect) + return nil +} + +// Publish will publish a message on a given topic to a connected MQTT broker. +// It will block until the message publish has succeeded or failed, and return an error if failed. +func (client *Clientv3) Publish( + topic string, + qos byte, + messagesRetained bool, + data []byte, +) error { + + client.Trace.With("topic", topic).With("qos", fmt.Sprintf("%b", qos)).Printf("publishing new message") + + token := client.conn.Publish(topic, qos, messagesRetained, data) + + select { + case <-client.ctx.Done(): + client.Debug.With("topic", topic).With("qos", fmt.Sprintf("%b", qos)).Printf("message publish cancelled due to context cancellation") + return nil + case <-token.Done(): + return token.Error() + } +} + +// Subscribe will subscribe to a given topic on a connected MQTT broker. +// An onReceived function will be registered and called any time a message is received from said broker. +func (client *Clientv3) Subscribe( + topic string, + qos byte, + onReceived func([]byte), +) error { + client.Debug.With("topic", topic).With("qos", fmt.Sprintf("%b", qos)).Printf("attempting new subscription") + + token := client.conn.Subscribe( + topic, + qos, + func(_ mqtt.Client, m mqtt.Message) { + client.Trace.With("topic", topic).With("qos", fmt.Sprintf("%b", qos)).Printf("message received from broker") + onReceived(m.Payload()) + }, + ) + + select { + case <-client.ctx.Done(): + client.Debug.With("topic", topic).With("qos", fmt.Sprintf("%b", qos)).Printf("subscription cancelled due to context cancellation") + return nil + case <-token.Done(): + return token.Error() + } +} + +// Unsubscribe will unsubscribe from a given topic of a connected MQTT broker. +func (client *Clientv3) Unsubscribe(topic string) error { + client.Debug.With("topic", topic).Printf("attempting to unsubscribe") + + token := client.conn.Unsubscribe(topic) + + select { + case <-client.ctx.Done(): + return nil + case <-token.Done(): + return token.Error() + } +} + +// MockClient is a PublisherSubscriber implementation used for testing purposes. +// It has callbacks which can be configured in tests to mock out client behaviors. +type MockClient struct { + OnDisconnect chan struct{} + OnConnect chan struct{} + OnSubscribe func(topic string, qos byte, onReceived func([]byte)) error + OnUnsubscribe func(topic string) error + OnPublish func(topic string, qos byte, messagesRetained bool, data []byte) error + OnGetName func() string + OnRender func() string + registry.Observable +} + +func (client *MockClient) GetName() string { + return client.OnGetName() +} + +func (client *MockClient) Render() string { + return client.OnRender() +} + +// Subscribe calls the mock client's OnSubscribe function, passing along its provided parameters. +func (client *MockClient) Subscribe( + topic string, + qos byte, + onReceived func([]byte), +) error { + return client.OnSubscribe(topic, qos, onReceived) +} + +// Publish calls the mock client's OnPublish function, passing along its provided parameters. +func (client *MockClient) Publish( + topic string, + qos byte, + messagesRetained bool, + data []byte, +) error { + return client.OnPublish(topic, qos, messagesRetained, data) +} + +// Unsubscribe calls the mock client's OnUnsubscribe function, passing along its provided parameters. +func (client *MockClient) Unsubscribe(topic string) error { + return client.OnUnsubscribe(topic) +} + +// Connect closes the mock client's OnConnect channel. +func (client *MockClient) Connect() error { + close(client.OnConnect) + return nil +} + +// Disconnect closes the mock client's OnDisconnect channel. +func (client *MockClient) Disconnect() error { + close(client.OnDisconnect) + return nil +} + +// Connected returns the mock client's OnConnect channel. +func (client *MockClient) Connected() chan struct{} { + return client.OnConnect +} + +// Disconnected returns the mock client's OnDisconnect channel. +func (client *MockClient) Disconnected() chan struct{} { + return client.OnDisconnect +} + +type MockV3Conn struct { + mqtt.Client + OnConnect func() mqtt.Token + OnDisconnect func(quiesce uint) + OnPublish func(topic string, qos byte, retained bool, payload interface{}) mqtt.Token + OnSubscribe func(topic string, qos byte, callback mqtt.MessageHandler) mqtt.Token + OnUnsubscribe func(topics ...string) mqtt.Token +} + +func (mock *MockV3Conn) Connect() mqtt.Token { + return mock.OnConnect() +} + +func (mock *MockV3Conn) Disconnect(quiesce uint) { + mock.OnDisconnect(quiesce) +} + +func (mock *MockV3Conn) Publish(topic string, qos byte, retained bool, payload interface{}) mqtt.Token { + return mock.OnPublish(topic, qos, retained, payload) +} + +func (mock *MockV3Conn) Subscribe(topic string, qos byte, callback mqtt.MessageHandler) mqtt.Token { + return mock.OnSubscribe(topic, qos, callback) +} + +func (mock *MockV3Conn) Unsubscribe(topics ...string) mqtt.Token { + return mock.OnUnsubscribe(topics...) +} + +type MockToken struct { + mqtt.Token + OnWait func() bool + OnWaitTimeout func(time.Duration) bool + OnDone func() <-chan struct{} + OnError func() error +} + +func (mock *MockToken) Wait() bool { + return mock.OnWait() +} + +func (mock *MockToken) WaitTimeout(t time.Duration) bool { + return mock.OnWaitTimeout(t) +} + +func (mock *MockToken) Done() <-chan struct{} { + return mock.OnDone() +} + +func (mock *MockToken) Error() error { + return mock.OnError() +} \ No newline at end of file diff --git a/samples/krill/components/client/client_test.go b/samples/krill/components/client/client_test.go new file mode 100644 index 0000000..4ffef39 --- /dev/null +++ b/samples/krill/components/client/client_test.go @@ -0,0 +1,533 @@ +package client + +import ( + "context" + "net" + "testing" + + mqttv5 "github.com/eclipse/paho.golang/paho" + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/iot-for-all/device-simulation/components/site" + "github.com/iot-for-all/device-simulation/lib/errors" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +var ( + exampleTopic = "example" +) + +func TestMockClient(t *testing.T) { + + client := &MockClient{ + OnDisconnect: make(chan struct{}), + OnConnect: make(chan struct{}), + OnSubscribe: func(topic string, qos byte, onReceived func([]byte)) error { + require.Equal(t, exampleTopic, topic) + return nil + }, OnUnsubscribe: func(topic string) error { + require.Equal(t, exampleTopic, topic) + return nil + }, OnPublish: func(topic string, qos byte, messagesRetained bool, data []byte) error { + require.Equal(t, exampleTopic, topic) + return nil + }, + } + + go func() { + require.NoError(t, client.Connect()) + }() + go func() { + require.NoError(t, client.Disconnect()) + }() + + <-client.Connected() + <-client.Disconnected() + + require.NoError(t, client.Publish(exampleTopic, 0, false, nil)) + require.NoError(t, client.Subscribe(exampleTopic, 0, func([]byte) {})) + require.NoError(t, client.Unsubscribe(exampleTopic)) +} + +type MockMQTTToken struct { + mqtt.Token + done chan struct{} + err error +} + +func (token *MockMQTTToken) Done() <-chan struct{} { + return token.done +} + +func (token *MockMQTTToken) Error() error { + return token.err +} + +type MockObserver struct { + onObserve func(float64) +} + +func (observer *MockObserver) Observe(value float64) { + observer.onObserve(value) +} + +type MockMessageHandler struct { + OnPayload func() []byte + mqtt.Message +} + +func (handler *MockMessageHandler) Payload() []byte { + return handler.OnPayload() +} + +func TestClientv3NoErrorsNoCancellations(t *testing.T) { + + ctx := context.Background() + mon := &MockObserver{} + broker := &MockObserver{} + onConnect := make(chan struct{}) + onPublish := make(chan struct{}) + onSub := make(chan struct{}) + onUnsub := make(chan struct{}) + sendSubscription := make(chan struct{}) + client := NewClientv3(&MockV3Conn{ + OnConnect: func() mqtt.Token { + return &MockMQTTToken{ + done: onConnect, + err: nil, + } + }, OnDisconnect: func(quiesce uint) { + + }, OnPublish: func(topic string, qos byte, retained bool, payload interface{}) mqtt.Token { + require.Equal(t, exampleTopic, topic) + return &MockMQTTToken{ + done: onPublish, + err: nil, + } + }, OnSubscribe: func(topic string, qos byte, callback mqtt.MessageHandler) mqtt.Token { + go func() { + <-sendSubscription + callback(nil, &MockMessageHandler{ + OnPayload: func() []byte { + return nil + }, + }) + }() + require.Equal(t, exampleTopic, topic) + return &MockMQTTToken{ + done: onSub, + err: nil, + } + }, OnUnsubscribe: func(topics ...string) mqtt.Token { + require.Equal(t, exampleTopic, topics[0]) + return &MockMQTTToken{ + done: onUnsub, + err: nil, + } + }, + }, *New(ctx, mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + + go func() { + close(onConnect) + }() + + go func() { + close(onPublish) + }() + + go func() { + close(onSub) + }() + + go func() { + close(onUnsub) + }() + + require.NoError(t, client.Connect()) + + <-client.Connected() + + require.NoError(t, client.Subscribe(exampleTopic, 0, func([]byte) {})) + + subscriptionReceived := make(chan struct{}) + + require.NoError(t, client.Subscribe(exampleTopic, 0, func([]byte) { + close(subscriptionReceived) + })) + + require.NoError(t, client.Publish(exampleTopic, 0, false, nil)) + + go func() { + require.NoError(t, client.Disconnect()) + }() + + <-client.Disconnected() + + go func() { + close(sendSubscription) + }() + + <-subscriptionReceived +} + +func TestClientv3WithContextCancellations(t *testing.T) { + + ctx, cancel := context.WithCancel(context.Background()) + mon := &MockObserver{} + broker := &MockObserver{} + onConnect := make(chan struct{}) + onPublish := make(chan struct{}) + onSub := make(chan struct{}) + onUnsub := make(chan struct{}) + client := NewClientv3(&MockV3Conn{ + OnConnect: func() mqtt.Token { + return &MockMQTTToken{ + done: onConnect, + err: errors.Mock{}, + } + }, OnDisconnect: func(quiesce uint) { + + }, OnPublish: func(topic string, qos byte, retained bool, payload interface{}) mqtt.Token { + require.Equal(t, exampleTopic, topic) + return &MockMQTTToken{ + done: onPublish, + err: errors.Mock{}, + } + }, OnSubscribe: func(topic string, qos byte, callback mqtt.MessageHandler) mqtt.Token { + require.Equal(t, exampleTopic, topic) + return &MockMQTTToken{ + done: onSub, + err: errors.Mock{}, + } + }, OnUnsubscribe: func(topics ...string) mqtt.Token { + require.Equal(t, exampleTopic, topics[0]) + return &MockMQTTToken{ + done: onUnsub, + err: errors.Mock{}, + } + }, + }, *New(ctx, mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + + go func() { + cancel() + }() + + require.NoError(t, client.Connect()) + + notConnected := make(chan struct{}) + + select { + case <-client.Connected(): + default: + close(notConnected) + } + + <-notConnected + + require.NoError(t, client.Subscribe(exampleTopic, 0, func([]byte) { + })) + + require.NoError(t, client.Unsubscribe(exampleTopic)) + + require.NoError(t, client.Publish(exampleTopic, 0, false, nil)) + + go func() { + require.NoError(t, client.Disconnect()) + }() + + notDisconnected := make(chan struct{}) + + select { + case <-client.Connected(): + default: + close(notDisconnected) + } + + <-notDisconnected +} + +func TestClientv3WithMockObserver(t *testing.T) { + observed := 101.101 + mon := &MockObserver{ + onObserve: func(f float64) { + require.Equal(t, observed, f) + }, + } + broker := &MockObserver{ + onObserve: func(f float64) { + require.Equal(t, observed, f) + }, + } + site := &site.MockSite{ + Observable: &MockObserver{ + onObserve: func(f float64) { + require.Equal(t, observed, f) + }, + }, + OnRender: func() string { + return "" + }, + } + client := NewClientv3(nil, *New(context.Background(), mon, broker, site)) + client.Observe(observed) +} + +func TestClientv3WithConnectionTokenError(t *testing.T) { + mon := &MockObserver{} + broker := &MockObserver{} + onConnect := make(chan struct{}) + client := NewClientv3(&MockV3Conn{ + OnConnect: func() mqtt.Token { + return &MockMQTTToken{ + done: onConnect, + err: errors.Mock{}, + } + }, + }, *New(context.Background(), mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + go func() { + close(onConnect) + }() + require.Equal(t, errors.Mock{}, client.Connect()) + + notConnected := make(chan struct{}) + + select { + case <-client.Connected(): + default: + close(notConnected) + } + + <-notConnected +} + +func TestClientv3WithUnsubscribeTokenError(t *testing.T) { + mon := &MockObserver{} + broker := &MockObserver{} + onUnsub := make(chan struct{}) + client := NewClientv3(&MockV3Conn{ + OnUnsubscribe: func(topics ...string) mqtt.Token { + return &MockMQTTToken{ + done: onUnsub, + err: errors.Mock{}, + } + }, + }, *New(context.Background(), mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + go func() { + close(onUnsub) + }() + require.Equal(t, errors.Mock{}, client.Unsubscribe("")) +} + +type MockConn struct { + net.Conn +} + +func TestClientv5ConnectAndDisconnect(t *testing.T) { + ctx := context.Background() + mon := &MockObserver{} + broker := &MockObserver{} + + client := NewClientv5(&MockV5Wrapper{ + OnConnect: func(ctx context.Context, cp *mqttv5.Connect) (*mqttv5.Connack, error) { + return nil, nil + }, + OnDisconnect: func(d *mqttv5.Disconnect) error { + return nil + }, + }, *New(ctx, mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + + require.NoError(t, client.Connect()) + + <-client.Connected() + + require.NoError(t, client.Disconnect()) + + <-client.Disconnected() +} + +func TestClientv5ConnectError(t *testing.T) { + ctx := context.Background() + mon := &MockObserver{} + broker := &MockObserver{} + + client := NewClientv5(&MockV5Wrapper{ + OnConnect: func(ctx context.Context, cp *mqttv5.Connect) (*mqttv5.Connack, error) { + return nil, errors.Mock{} + }, + }, *New(ctx, mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + + require.Equal(t, errors.Mock{}, client.Connect()) +} + +func TestClientv5DisconnectError(t *testing.T) { + ctx := context.Background() + mon := &MockObserver{} + broker := &MockObserver{} + + client := NewClientv5(&MockV5Wrapper{ + OnDisconnect: func(d *mqttv5.Disconnect) error { + return errors.Mock{} + }, + }, *New(ctx, mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + + require.Equal(t, errors.Mock{}, client.Disconnect()) +} + +func TestClientv5Publish(t *testing.T) { + ctx := context.Background() + mon := &MockObserver{} + broker := &MockObserver{} + + client := NewClientv5(&MockV5Wrapper{ + OnPublish: func(ctx context.Context, p *mqttv5.Publish) (*mqttv5.PublishResponse, error) { + return nil, errors.Mock{} + }, + }, *New(ctx, mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + + require.Equal(t, errors.Mock{}, client.Publish("", 0, false, nil)) +} + +func TestClientv5Subscribe(t *testing.T) { + ctx := context.Background() + mon := &MockObserver{} + broker := &MockObserver{} + + client := NewClientv5(&MockV5Wrapper{ + OnSubscribe: func(ctx context.Context, s *mqttv5.Subscribe) (*mqttv5.Suback, error) { + return nil, nil + }, OnRegisterHandler: func(s string, mh mqttv5.MessageHandler) {}, + }, *New(ctx, mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + + require.NoError(t, client.Subscribe("", 0, func([]byte) {})) +} + +func TestClientv5SubscribeError(t *testing.T) { + ctx := context.Background() + mon := &MockObserver{} + broker := &MockObserver{} + + client := NewClientv5(&MockV5Wrapper{ + OnSubscribe: func(ctx context.Context, s *mqttv5.Subscribe) (*mqttv5.Suback, error) { + return nil, errors.Mock{} + }, + }, *New(ctx, mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + + require.Equal(t, errors.Mock{}, client.Subscribe("", 0, func([]byte) {})) +} + +func TestClientv5Unsubscribe(t *testing.T) { + ctx := context.Background() + mon := &MockObserver{} + broker := &MockObserver{} + + client := NewClientv5(&MockV5Wrapper{ + OnUnsubscribe: func(ctx context.Context, u *mqttv5.Unsubscribe) (*mqttv5.Unsuback, error) { + return nil, nil + }, + OnUnregisterHandler: func(string) {}, + }, *New(ctx, mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + + require.NoError(t, client.Unsubscribe("")) +} + +func TestClientv5UnsubscribeError(t *testing.T) { + ctx := context.Background() + mon := &MockObserver{} + broker := &MockObserver{} + + client := NewClientv5(&MockV5Wrapper{ + OnUnsubscribe: func(ctx context.Context, u *mqttv5.Unsubscribe) (*mqttv5.Unsuback, error) { + return nil, errors.Mock{} + }, OnUnregisterHandler: func(s string) {}, + }, *New(ctx, mon, broker, &site.MockSite{ + OnRender: func() string { + return "" + }, + })) + + require.Equal(t, errors.Mock{}, client.Unsubscribe("")) +} + +func TestMockClientv5Wrapper(t *testing.T) { + client := &MockV5Wrapper{ + OnConnect: func(ctx context.Context, cp *mqttv5.Connect) (*mqttv5.Connack, error) { + return nil, nil + }, + OnDisconnect: func(d *mqttv5.Disconnect) error { + return nil + }, + OnPublish: func(ctx context.Context, p *mqttv5.Publish) (*mqttv5.PublishResponse, error) { + return nil, nil + }, + OnSubscribe: func(ctx context.Context, s *mqttv5.Subscribe) (*mqttv5.Suback, error) { + return nil, nil + }, + OnUnsubscribe: func(ctx context.Context, u *mqttv5.Unsubscribe) (*mqttv5.Unsuback, error) { + return nil, nil + }, + OnRegisterHandler: func(string, mqttv5.MessageHandler) { + }, + OnUnregisterHandler: func(string) { + }, + } + + _, err := client.Connect(context.TODO(), nil) + require.NoError(t, err) + _, err = client.Publish(context.TODO(), nil) + require.NoError(t, err) + _, err = client.Subscribe(context.TODO(), nil) + require.NoError(t, err) + _, err = client.Unsubscribe(context.TODO(), nil) + require.NoError(t, err) + require.NoError(t, client.Disconnect(nil)) + client.RegisterHandler("", nil) + client.UnregisterHandler("") + +} \ No newline at end of file diff --git a/samples/krill/components/client/clientv5.go b/samples/krill/components/client/clientv5.go new file mode 100644 index 0000000..6173fbd --- /dev/null +++ b/samples/krill/components/client/clientv5.go @@ -0,0 +1,261 @@ +package client + +import ( + "context" + "fmt" + + "github.com/eclipse/paho.golang/paho" + "github.com/iot-for-all/device-simulation/lib/logger" +) + +// Clientv5 is a full PublisherSubscriber implementation which follows the MQTTv5 client protocol. +type Clientv5 struct { + conn V5Conn + Client +} + +// NewClientv5 creates a Clientv5, given a V5Conn wrapper (wrapper around paho v5 connection) and an underlying client. +func NewClientv5( + conn V5Conn, + cli Client, +) *Clientv5 { + return &Clientv5{ + conn: conn, + Client: cli, + } +} + +// Connect establishes a connection with an MQTT v5 compatible broker. +// It will also close the onConnect channel if the connection succeeds. +func (client *Clientv5) Connect() error { + client.Debug.Printf("attempting new connection with broker") + + _, err := client.conn.Connect(client.ctx, &paho.Connect{ + KeepAlive: 5, + CleanStart: true, + }) + if err != nil { + client.Logger.Level(logger.Error).With("error", err.Error()).Printf("an error occurred when connecting to the broker") + return err + } + close(client.onConnect) + return err +} + +// Disconnect will disconnect from a connected MQTT broker. +// It will also close the onDisconnect channel upon completion. +func (client *Clientv5) Disconnect() error { + client.Debug.Printf("attempting to disconnect from broker") + err := client.conn.Disconnect(&paho.Disconnect{}) + if err != nil { + client.Logger.Level(logger.Error).With("error", err.Error()).Printf("an error occurred when disconnecting from the broker") + return err + } + close(client.onDisconnect) + return nil +} + +// Publish will publish a message on a given topic to a connected MQTT broker. +// It will block until the message publish has succeeded or failed, and return an error if failed. +func (client *Clientv5) Publish( + topic string, + qos byte, + messagesRetained bool, + data []byte, +) error { + client.Trace.With("topic", topic).With("qos", fmt.Sprintf("%b", qos)).Printf("publishing new message") + _, err := client.conn.Publish(client.ctx, &paho.Publish{ + QoS: qos, + Retain: messagesRetained, + Topic: topic, + Payload: data, + }) + if err != nil { + client.Logger.Level(logger.Error).With("error", err.Error()).With("topic", topic).With("qos", fmt.Sprintf("%b", qos)).Printf("message publish failed") + return err + } + + return nil +} + +// Subscribe will subscribe to a given topic on a connected MQTT broker. +// An onReceived function will be registered and called any time a message is received from said broker. +func (client *Clientv5) Subscribe( + topic string, + qos byte, + onReceived func([]byte), +) error { + client.Debug.With("topic", topic).With("qos", fmt.Sprintf("%b", qos)).Printf("attempting new subscription") + + _, err := client.conn.Subscribe(client.ctx, &paho.Subscribe{ + Subscriptions: map[string]paho.SubscribeOptions{ + topic: { + QoS: qos, + }, + }, + }) + if err != nil { + client.Logger.Level(logger.Error).With("error", err.Error()).With("topic", topic).With("qos", fmt.Sprintf("%b", qos)).Printf("failed to subscribe to the broker") + return err + } + + client.conn.RegisterHandler(topic, func(p *paho.Publish) { + client.Trace.With("topic", topic).With("qos", fmt.Sprintf("%b", qos)).Printf("message received from broker") + onReceived(p.Payload) + }) + + return nil +} + +// Unsubscribe will unsubscribe from a given topic of a connected MQTT broker. +func (client *Clientv5) Unsubscribe(topic string) error { + client.Debug.With("topic", topic).Printf("attempting to unsubscribe") + + client.conn.UnregisterHandler(topic) + + _, err := client.conn.Unsubscribe(client.ctx, &paho.Unsubscribe{ + Topics: []string{topic}, + }) + if err != nil { + client.Logger.Level(logger.Error).With("error", err.Error()).With("topic", topic).Printf("failed to unsubscribe from the broker") + return err + } + + return nil +} + +// V5Conn is an interface whose implementation should be a wrapper around the paho v5 client functionality. +// The purpose of the interface and wrapper is for testing/mocking purposes, and the paho v5 client is difficult +// to mock due to the underlying TCP connection it depends on. +type V5Conn interface { + Connect(ctx context.Context, cp *paho.Connect) (*paho.Connack, error) + Disconnect(d *paho.Disconnect) error + Publish(ctx context.Context, p *paho.Publish) (*paho.PublishResponse, error) + Subscribe(ctx context.Context, s *paho.Subscribe) (*paho.Suback, error) + Unsubscribe( + ctx context.Context, + u *paho.Unsubscribe, + ) (*paho.Unsuback, error) + RegisterHandler(string, paho.MessageHandler) + UnregisterHandler(string) +} + +// V5Wrapper is an implementation of V5Conn which wraps the functionality of the paho v5 client. +type V5Wrapper struct { + conn *paho.Client +} + +// NewV5Wrapper creates a new V5Wrapper given a paho v5 client. +func NewV5Wrapper(conn *paho.Client) V5Conn { + return &V5Wrapper{ + conn: conn, + } +} + +// Connect sets the clientID in the paho connect packet cp equal to the ID of the paho client. +// It then calls the paho client's connect function, passing along its parameters. +func (wrapper *V5Wrapper) Connect( + ctx context.Context, + cp *paho.Connect, +) (*paho.Connack, error) { + cp.ClientID = wrapper.conn.ClientID + return wrapper.conn.Connect(ctx, cp) +} + +// Disconnect calls the paho client's disconnect function, passing along its disconnect packet parameter. +func (wrapper *V5Wrapper) Disconnect(d *paho.Disconnect) error { + return wrapper.conn.Disconnect(d) +} + +// Publish calls the paho client's publish function, passing through its context and publish packet parameters. +func (wrapper *V5Wrapper) Publish( + ctx context.Context, + p *paho.Publish, +) (*paho.PublishResponse, error) { + return wrapper.conn.Publish(ctx, p) +} + +// Subscribe calls the paho client's subscribe function, passing through its context and subscribe packet parameters. +func (wrapper *V5Wrapper) Subscribe( + ctx context.Context, + s *paho.Subscribe, +) (*paho.Suback, error) { + return wrapper.conn.Subscribe(ctx, s) +} + +// Unsubscribe calls the paho client's unsubscribe function, passing through its context and unsubscribe packet parameters. +func (wrapper *V5Wrapper) Unsubscribe( + ctx context.Context, + u *paho.Unsubscribe, +) (*paho.Unsuback, error) { + return wrapper.conn.Unsubscribe(ctx, u) +} + +// RegisterHandler calls the paho client's register handler function, passing through a provided topic, and a message handler. +func (wrapper *V5Wrapper) RegisterHandler(topic string, p paho.MessageHandler) { + wrapper.conn.Router.RegisterHandler(topic, p) +} + +// UnregisterHandler calls the paho client's unregister handler function, passing through a provided topic. +func (wrapper *V5Wrapper) UnregisterHandler(topic string) { + wrapper.conn.Router.UnregisterHandler(topic) +} + +// MockV5Wrapper is a mocking struct designed for testing. +// It implements the V5Conn interface. +type MockV5Wrapper struct { + OnConnect func(ctx context.Context, cp *paho.Connect) (*paho.Connack, error) + OnDisconnect func(d *paho.Disconnect) error + OnPublish func(ctx context.Context, p *paho.Publish) (*paho.PublishResponse, error) + OnSubscribe func(ctx context.Context, s *paho.Subscribe) (*paho.Suback, error) + OnUnsubscribe func(ctx context.Context, u *paho.Unsubscribe) (*paho.Unsuback, error) + OnRegisterHandler func(string, paho.MessageHandler) + OnUnregisterHandler func(string) +} + +// Connect calls the OnConnect function, passing through its parameters. +func (wrapper *MockV5Wrapper) Connect( + ctx context.Context, + cp *paho.Connect, +) (*paho.Connack, error) { + return wrapper.OnConnect(ctx, cp) +} + +// Connect calls the OnDisconnect function, passing through its parameters. +func (wrapper *MockV5Wrapper) Disconnect(d *paho.Disconnect) error { + return wrapper.OnDisconnect(d) +} + +// Publish calls the OnPublish function, passing through its parameters. +func (wrapper *MockV5Wrapper) Publish( + ctx context.Context, + p *paho.Publish, +) (*paho.PublishResponse, error) { + return wrapper.OnPublish(ctx, p) +} + +// Subscribe calls the OnSubscribe function, passing through its parameters. +func (wrapper *MockV5Wrapper) Subscribe( + ctx context.Context, + s *paho.Subscribe, +) (*paho.Suback, error) { + return wrapper.OnSubscribe(ctx, s) +} + +// Unsubscribe calls the OnUnsubscribe function, passing through its parameters. +func (wrapper *MockV5Wrapper) Unsubscribe( + ctx context.Context, + u *paho.Unsubscribe, +) (*paho.Unsuback, error) { + return wrapper.OnUnsubscribe(ctx, u) +} + +// RegisterHandler calls the OnRegisterHandler function, passing through its parameters. +func (wrapper *MockV5Wrapper) RegisterHandler(s string, p paho.MessageHandler) { + wrapper.OnRegisterHandler(s, p) +} + +// UnregisterHandler calls the OnUnregisterHandler function, passing through its parameters. +func (wrapper *MockV5Wrapper) UnregisterHandler(s string) { + wrapper.OnUnregisterHandler(s) +} diff --git a/samples/krill/components/client/errors.go b/samples/krill/components/client/errors.go new file mode 100644 index 0000000..bbd8a16 --- /dev/null +++ b/samples/krill/components/client/errors.go @@ -0,0 +1,32 @@ +package client + +import ( + "fmt" + + "github.com/iot-for-all/device-simulation/lib/errors" +) + +type BrokerConnectionError struct { + errors.BadRequest + id string + endpoint string + err error +} + +func (err *BrokerConnectionError) Error() string { + return fmt.Sprintf( + "mqtt client with id=%s could not connect to MQTT broker at endpoint %s: %q", + err.id, + err.endpoint, + err.err.Error(), + ) +} + +type UnknownClientTypeError struct { + errors.BadRequest + name string +} + +func (err *UnknownClientTypeError) Error() string { + return fmt.Sprintf("no such %s type for mqtt client component", err.name) +} \ No newline at end of file diff --git a/samples/krill/components/client/service.go b/samples/krill/components/client/service.go new file mode 100644 index 0000000..3e0aa59 --- /dev/null +++ b/samples/krill/components/client/service.go @@ -0,0 +1,146 @@ +package client + +import ( + "context" + + "github.com/iot-for-all/device-simulation/components/broker" + "github.com/iot-for-all/device-simulation/components/observer" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/site" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/dialer" + "github.com/iot-for-all/device-simulation/lib/logger" + + mqttv5 "github.com/eclipse/paho.golang/paho" + mqttv3 "github.com/eclipse/paho.mqtt.golang" +) + +type Store component.Store[PublisherSubscriber, component.ID] + +type Type string + +const ( + V3 Type = "v3" + V5 Type = "v5" +) + +type Component struct { + RegistryID component.ID + BrokerID component.ID + SiteID component.ID + Name string + Password string + Username string + ConnectionRetries int + Type Type +} + +type Service struct { + Store + registryStore registry.Store + brokerStore broker.Store + siteStore site.Store + Dialer dialer.Dialer + Logger logger.Logger + CreateV5Conn func(conn *mqttv5.Client) V5Conn + CreateV3Conn func(o *mqttv3.ClientOptions) mqttv3.Client + ctx context.Context +} + +func NewStore() Store { + return component.New[PublisherSubscriber, component.ID]() +} + +func NewService( + ctx context.Context, + store Store, + registryStore registry.Store, + brokerStore broker.Store, + siteStore site.Store, + options ...func(*Service), +) *Service { + service := &Service{ + Store: store, + registryStore: registryStore, + brokerStore: brokerStore, + siteStore: siteStore, + Dialer: dialer.New(), + Logger: &logger.NoopLogger{}, + CreateV5Conn: NewV5Wrapper, + CreateV3Conn: mqttv3.NewClient, + ctx: ctx, + } + + for _, option := range options { + option(service) + } + + return service +} + +func (service *Service) Create(id component.ID, c *Component) error { + var reg registry.Observable + reg, err := service.registryStore.Get(c.RegistryID) + if err != nil { + _, ok := err.(*component.NotFoundError) + if !ok { + return err + } + reg = &observer.NoopObservable{} + } + + brkr, err := service.brokerStore.Get(c.BrokerID) + if err != nil { + return err + } + + ste, err := service.siteStore.Get(c.SiteID) + if err != nil { + return err + } + + base := New(service.ctx, reg, brkr, ste, func(cli *Client) { + cli.Name = c.Name + cli.Logger = service.Logger.With("name", c.Name).With("site", ste.Render()).With("mqtt_version", string(c.Type)).With("broker_endpoint", brkr.Endpoint()) + }) + var cli PublisherSubscriber + switch c.Type { + case V5: + conn, err := service.Dialer.Dial("tcp", brkr.Endpoint()) + if err != nil { + return &BrokerConnectionError{ + id: string(id), + endpoint: brkr.Endpoint(), + err: err, + } + } + + pcli := mqttv5.NewClient(mqttv5.ClientConfig{ + Router: mqttv5.NewStandardRouter(), + Conn: conn, + ClientID: c.Name, + }) + + cli = NewClientv5(service.CreateV5Conn(pcli), *base) + case V3: + opt := mqttv3.NewClientOptions() + opt.AddBroker(brkr.Endpoint()) + opt.SetClientID(c.Name) + opt.SetUsername(c.Username) + opt.SetPassword(c.Password) + opt.SetCleanSession(true) + + cli = NewClientv3(service.CreateV3Conn(opt), *base) + default: + return &UnknownClientTypeError{ + name: string(c.Type), + } + } + + err = cli.Connect() + if err != nil { + return err + } + + return service.Store.Create(cli, id) +} diff --git a/samples/krill/components/client/service_test.go b/samples/krill/components/client/service_test.go new file mode 100644 index 0000000..9786eb4 --- /dev/null +++ b/samples/krill/components/client/service_test.go @@ -0,0 +1,397 @@ +package client + +import ( + "context" + "net" + "testing" + + "github.com/iot-for-all/device-simulation/components/broker" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/site" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/dialer" + "github.com/stretchr/testify/require" + + mqttv5 "github.com/eclipse/paho.golang/paho" + mqttv3 "github.com/eclipse/paho.mqtt.golang" +) + +const ( + MockID = "MockID" + MockRegistryID = "MockRegistryID" + MockBrokerID = "MockBrokerID" + MockSiteID = "MockSiteID" + MockEndpoint = "MockEndpoint" + MockSite = "MockSite" + MockName = "MockName" + MockPassword = "MockPassword" + MockUsername = "MockUsername" + MockType = "MockType" + MockError = "MockError" +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[PublisherSubscriber, component.ID]) + require.True(t, ok) +} + +func TestServiceClientV5(t *testing.T) { + + ctx := context.Background() + + service := NewService(ctx, &component.MockStore[PublisherSubscriber, component.ID]{ + OnCreate: func(entity PublisherSubscriber, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return ®istry.MockRegistry{}, nil + }, + }, &component.MockStore[broker.Source, component.ID]{ + OnGet: func(identifier component.ID) (broker.Source, error) { + require.Equal(t, MockBrokerID, string(identifier)) + return &broker.MockBroker{ + OnEndpoint: func() string { + return MockEndpoint + }, + }, nil + }, + }, &component.MockStore[site.Site, component.ID]{ + OnGet: func(identifier component.ID) (site.Site, error) { + require.Equal(t, MockSiteID, string(identifier)) + return &site.MockSite{ + OnRender: func() string { + return MockSite + }, + }, nil + }, + }, func(s *Service) { + s.Dialer = &dialer.MockDialer{ + OnDial: func(network, address string) (net.Conn, error) { + return &dialer.NoopConn{}, nil + }, + } + s.CreateV5Conn = func(conn *mqttv5.Client) V5Conn { + return &MockV5Wrapper{ + OnConnect: func(ctx context.Context, cp *mqttv5.Connect) (*mqttv5.Connack, error) { + return nil, nil + }, + } + } + }) + + err := service.Create(MockID, &Component{ + RegistryID: MockRegistryID, + BrokerID: MockBrokerID, + SiteID: MockSiteID, + Type: V5, + }) + require.NoError(t, err) +} + +func TestServiceClientV3(t *testing.T) { + + ctx := context.Background() + + service := NewService(ctx, &component.MockStore[PublisherSubscriber, component.ID]{ + OnCreate: func(entity PublisherSubscriber, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return ®istry.MockRegistry{}, nil + }, + }, &component.MockStore[broker.Source, component.ID]{ + OnGet: func(identifier component.ID) (broker.Source, error) { + require.Equal(t, MockBrokerID, string(identifier)) + return &broker.MockBroker{ + OnEndpoint: func() string { + return MockEndpoint + }, + }, nil + }, + }, &component.MockStore[site.Site, component.ID]{ + OnGet: func(identifier component.ID) (site.Site, error) { + require.Equal(t, MockSiteID, string(identifier)) + return &site.MockSite{ + OnRender: func() string { + return MockSite + }, + }, nil + }, + }, func(s *Service) { + s.CreateV3Conn = func(o *mqttv3.ClientOptions) mqttv3.Client { + require.Equal(t, MockEndpoint, o.Servers[0].Host) + require.Equal(t, MockName, o.ClientID) + require.Equal(t, MockPassword, o.Password) + require.Equal(t, MockUsername, o.Username) + require.True(t, o.CleanSession) + return &MockV3Conn{ + OnConnect: func() mqttv3.Token { + return &MockToken{ + OnDone: func() <-chan struct{} { + c := make(chan struct{}) + close(c) + return c + }, OnError: func() error { + return nil + }, + } + }, + } + } + }) + + err := service.Create(MockID, &Component{ + RegistryID: MockRegistryID, + BrokerID: MockBrokerID, + SiteID: MockSiteID, + Name: MockName, + Password: MockPassword, + Username: MockUsername, + ConnectionRetries: 0, + Type: V3, + }) + require.NoError(t, err) +} + +func TestServiceInvalidClientType(t *testing.T) { + + ctx := context.Background() + + service := NewService(ctx, &component.MockStore[PublisherSubscriber, component.ID]{ + OnCreate: func(entity PublisherSubscriber, identifier component.ID) error { + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return ®istry.MockRegistry{}, nil + }, + }, &component.MockStore[broker.Source, component.ID]{ + OnGet: func(identifier component.ID) (broker.Source, error) { + return &broker.MockBroker{ + OnEndpoint: func() string { + return MockEndpoint + }, + }, nil + }, + }, &component.MockStore[site.Site, component.ID]{ + OnGet: func(identifier component.ID) (site.Site, error) { + return &site.MockSite{ + OnRender: func() string { + return MockSite + }, + }, nil + }, + }) + + err := service.Create(MockID, &Component{ + RegistryID: MockRegistryID, + BrokerID: MockBrokerID, + SiteID: MockSiteID, + Name: MockName, + Password: MockPassword, + Username: MockUsername, + ConnectionRetries: 0, + Type: MockType, + }) + require.Equal(t, &UnknownClientTypeError{ + name: MockType, + }, err) +} + +func TestServiceClientV3ConnectionError(t *testing.T) { + + ctx := context.Background() + + service := NewService(ctx, &component.MockStore[PublisherSubscriber, component.ID]{ + OnCreate: func(entity PublisherSubscriber, identifier component.ID) error { + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return ®istry.MockRegistry{}, nil + }, + }, &component.MockStore[broker.Source, component.ID]{ + OnGet: func(identifier component.ID) (broker.Source, error) { + return &broker.MockBroker{ + OnEndpoint: func() string { + return MockEndpoint + }, + }, nil + }, + }, &component.MockStore[site.Site, component.ID]{ + OnGet: func(identifier component.ID) (site.Site, error) { + return &site.MockSite{ + OnRender: func() string { + return MockSite + }, + }, nil + }, + }, func(s *Service) { + s.CreateV3Conn = func(o *mqttv3.ClientOptions) mqttv3.Client { + return &MockV3Conn{ + OnConnect: func() mqttv3.Token { + return &MockToken{ + OnDone: func() <-chan struct{} { + c := make(chan struct{}) + close(c) + return c + }, OnError: func() error { + return &component.MockError{ + OnError: func() string { + return "" + }, + } + }, + } + }, + } + } + }) + + err := service.Create(MockID, &Component{ + Type: V3, + }) + _, ok := err.(*component.MockError) + require.True(t, ok) +} + +func TestServiceClientV5DialError(t *testing.T) { + + ctx := context.Background() + + service := NewService(ctx, &component.MockStore[PublisherSubscriber, component.ID]{ + OnCreate: func(entity PublisherSubscriber, identifier component.ID) error { + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return ®istry.MockRegistry{}, nil + }, + }, &component.MockStore[broker.Source, component.ID]{ + OnGet: func(identifier component.ID) (broker.Source, error) { + return &broker.MockBroker{ + OnEndpoint: func() string { + return MockEndpoint + }, + }, nil + }, + }, &component.MockStore[site.Site, component.ID]{ + OnGet: func(identifier component.ID) (site.Site, error) { + return &site.MockSite{ + OnRender: func() string { + return MockSite + }, + }, nil + }, + }, func(s *Service) { + s.Dialer = &dialer.MockDialer{ + OnDial: func(network, address string) (net.Conn, error) { + return nil, &component.MockError{} + }, + } + }) + + err := service.Create(MockID, &Component{ + Type: V5, + }) + require.Equal(t, &BrokerConnectionError{ + id: MockID, + err: &component.MockError{}, + endpoint: MockEndpoint, + }, err) +} + +func TestServiceRegistryStoreGetError(t *testing.T) { + + ctx := context.Background() + + service := NewService(ctx, nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.MockError{} + }, + }, nil, nil) + + err := service.Create(MockID, &Component{ + Type: V5, + }) + require.Equal(t, &component.MockError{}, err) +} + +func TestServiceBrokerError(t *testing.T) { + + ctx := context.Background() + + service := NewService(ctx, nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }, &component.MockStore[broker.Source, component.ID]{ + OnGet: func(identifier component.ID) (broker.Source, error) { + return nil, &component.MockError{} + }, + }, nil) + + err := service.Create(MockID, &Component{ + Type: V5, + }) + require.Equal(t, &component.MockError{}, err) +} + +func TestServiceSiteErrorRegistryNotFound(t *testing.T) { + + ctx := context.Background() + + service := NewService(ctx, nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.NotFoundError{} + }, + }, &component.MockStore[broker.Source, component.ID]{ + OnGet: func(identifier component.ID) (broker.Source, error) { + return nil, nil + }, + }, &component.MockStore[site.Site, component.ID]{ + OnGet: func(identifier component.ID) (site.Site, error) { + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{ + Type: V5, + }) + require.Equal(t, &component.MockError{}, err) +} + +func TestBrokerConnectionError(t *testing.T) { + + expectedError := `mqtt client with id=MockID could not connect to MQTT broker at endpoint MockEndpoint: "MockError"` + + err := &BrokerConnectionError{ + id: MockID, + endpoint: MockEndpoint, + err: &component.MockError{ + OnError: func() string { + return MockError + }, + }, + } + + require.Equal(t, expectedError, err.Error()) +} + +func TestUnknownClientTypeError(t *testing.T) { + + expectedError := "no such MockName type for mqtt client component" + + err := &UnknownClientTypeError{ + name: MockName, + } + + require.Equal(t, expectedError, err.Error()) +} \ No newline at end of file diff --git a/samples/krill/components/edge/edge_test.go b/samples/krill/components/edge/edge_test.go new file mode 100644 index 0000000..1fcaf14 --- /dev/null +++ b/samples/krill/components/edge/edge_test.go @@ -0,0 +1,202 @@ +package edge + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/composition" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockParentNodeID = "MockParentNodeID" + MockChildNodeID = "MockChildNodeID" + MockLabelEdgeConfiguration = "MockLabelEdgeConfiguration" + MockPositionEdgeConfiguration = 5 + MockInvalidType = "MockInvalidType" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[composition.Edge, component.ID]) + require.True(t, ok) +} + +func TestEdgeServiceLabel(t *testing.T) { + service := NewService(&component.MockStore[composition.Edge, component.ID]{ + OnCreate: func(entity composition.Edge, identifier component.ID) error { + return nil + }, + }, &component.MockStore[composition.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (composition.Renderer, error) { + if identifier == MockParentNodeID { + return &composition.MockNode{ + OnWith: func(e composition.Edge) composition.Node { + res, ok := e.(*composition.Label) + require.True(t, ok) + require.Equal(t, MockLabelEdgeConfiguration, res.Edge()) + return nil + }, + }, nil + } else { + require.Equal(t, MockChildNodeID, string(identifier)) + } + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{ + ParentNodeId: MockParentNodeID, + ChildNodeId: MockChildNodeID, + Type: LABEL, + Configuration: MockLabelEdgeConfiguration, + }) + require.NoError(t, err) +} + +func TestEdgeServicePosition(t *testing.T) { + service := NewService(&component.MockStore[composition.Edge, component.ID]{ + OnCreate: func(entity composition.Edge, identifier component.ID) error { + return nil + }, + }, &component.MockStore[composition.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (composition.Renderer, error) { + if identifier == MockParentNodeID { + return &composition.MockNode{ + OnWith: func(e composition.Edge) composition.Node { + res, ok := e.(*composition.Position) + require.True(t, ok) + require.Equal(t, MockPositionEdgeConfiguration, res.Edge()) + return nil + }, + }, nil + } else { + require.Equal(t, MockChildNodeID, string(identifier)) + } + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{ + ParentNodeId: MockParentNodeID, + ChildNodeId: MockChildNodeID, + Type: POSITION, + Configuration: MockPositionEdgeConfiguration, + }) + require.NoError(t, err) +} + +func TestEdgeServiceInvalidEdgeType(t *testing.T) { + service := NewService(nil, &component.MockStore[composition.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (composition.Renderer, error) { + return &composition.MockNode{}, nil + }, + }) + + err := service.Create(MockID, &Component{ + ParentNodeId: MockParentNodeID, + Type: MockInvalidType, + }) + require.Equal(t, &InvalidTypeError{ + kind: MockInvalidType, + identifier: MockID, + }, err) +} + +func TestEdgeServiceIdentifierConflict(t *testing.T) { + service := NewService(nil, nil) + + err := service.Create(MockID, &Component{ + ParentNodeId: MockParentNodeID, + ChildNodeId: MockParentNodeID, + Type: MockInvalidType, + }) + require.Equal(t, &IdentifierConflictError{ + invalid: MockParentNodeID, + identifier: MockID, + }, err) +} + +func TestEdgeServiceParentNodeStoreGetError(t *testing.T) { + service := NewService(nil, &component.MockStore[composition.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (composition.Renderer, error) { + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{ + ParentNodeId: MockParentNodeID, + }) + require.Equal(t, &component.MockError{}, err) +} + +func TestEdgeServiceChildNodeStoreGetError(t *testing.T) { + service := NewService(nil, &component.MockStore[composition.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (composition.Renderer, error) { + if identifier == MockParentNodeID { + return &composition.MockNode{}, nil + } + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{ + ParentNodeId: MockParentNodeID, + }) + require.Equal(t, &component.MockError{}, err) +} + +func TestEdgeServiceInvalidParentNodeType(t *testing.T) { + service := NewService(nil, &component.MockStore[composition.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (composition.Renderer, error) { + return &composition.MockRenderer{}, nil + }, + }) + + err := service.Create(MockID, &Component{ + ParentNodeId: MockParentNodeID, + }) + require.Equal(t, &InvalidParentNodeTypeError{ + identifier: MockID, + parentNodeIdentifier: MockParentNodeID, + }, err) +} + +func TestEdgeServiceInvalidLabelConfiguration(t *testing.T) { + service := NewService(nil, &component.MockStore[composition.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (composition.Renderer, error) { + return &composition.MockNode{}, nil + }, + }) + + err := service.Create(MockID, &Component{ + ParentNodeId: MockParentNodeID, + Configuration: MockPositionEdgeConfiguration, + Type: LABEL, + }) + require.Equal(t, &InvalidLabelError{ + identifier: MockID, + }, err) +} + +func TestEdgeServiceInvalidPositionConfiguration(t *testing.T) { + service := NewService(nil, &component.MockStore[composition.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (composition.Renderer, error) { + return &composition.MockNode{}, nil + }, + }) + + err := service.Create(MockID, &Component{ + ParentNodeId: MockParentNodeID, + Configuration: MockLabelEdgeConfiguration, + Type: POSITION, + }) + require.Equal(t, &InvalidPositionError{ + identifier: MockID, + }, err) +} diff --git a/samples/krill/components/edge/errors.go b/samples/krill/components/edge/errors.go new file mode 100644 index 0000000..b0c0560 --- /dev/null +++ b/samples/krill/components/edge/errors.go @@ -0,0 +1,61 @@ +package edge + +import ( + "fmt" + + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/errors" +) + + +type InvalidPositionError struct { + errors.BadRequest + identifier component.ID +} + +func (err *InvalidPositionError) Error() string { + return fmt.Sprintf("could not create the edge with id %q -- for a position type edge, an integer value must be provided", err.identifier) +} + +type InvalidLabelError struct { + errors.BadRequest + identifier component.ID +} + +func (err *InvalidLabelError) Error() string { + return fmt.Sprintf("could not create the edge with id %q -- for a label type edge, a string value must be provided", err.identifier) +} + +type InvalidTypeError struct { + errors.BadRequest + kind string + identifier component.ID +} + +func (err *InvalidTypeError) Error() string { + return fmt.Sprintf( + "attempted to create a edge (identifier %s) with an invalid edge type %s", + err.identifier, + err.kind, + ) +} + +type InvalidParentNodeTypeError struct { + errors.BadRequest + identifier component.ID + parentNodeIdentifier component.ID +} + +func (err *InvalidParentNodeTypeError) Error() string { + return fmt.Sprintf("the parent node with id %s of the edge with id %s must be of type collection of array", err.parentNodeIdentifier, err.identifier) +} + +type IdentifierConflictError struct { + errors.BadRequest + identifier component.ID + invalid component.ID +} + +func (err *IdentifierConflictError) Error() string { + return fmt.Sprintf("edge with id %s cannot have identical child and parent identifiers (id %s)", err.identifier, err.invalid) +} \ No newline at end of file diff --git a/samples/krill/components/edge/service.go b/samples/krill/components/edge/service.go new file mode 100644 index 0000000..5581107 --- /dev/null +++ b/samples/krill/components/edge/service.go @@ -0,0 +1,97 @@ +package edge + +import ( + "github.com/iot-for-all/device-simulation/components/node" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/composition" +) + +type Store component.Store[composition.Edge, component.ID] + +type Type string + +const ( + LABEL Type = "label" + POSITION Type = "position" +) + +type Component struct { + ParentNodeId component.ID + ChildNodeId component.ID + Type Type + Configuration any +} + +func NewStore() Store { + return component.New[composition.Edge, component.ID]() +} + +type Service struct { + Store + nodeStore node.Store +} + +func NewService(store Store, nodeStore node.Store) *Service { + return &Service{ + Store: store, + nodeStore: nodeStore, + } +} + +func (service *Service) Create(id component.ID, c *Component) error { + if c.ParentNodeId == c.ChildNodeId { + return &IdentifierConflictError{ + identifier: id, + invalid: c.ChildNodeId, + } + } + + parent, err := service.nodeStore.Get(c.ParentNodeId) + if err != nil { + return err + } + + parentNode, ok := parent.(composition.Node) + if !ok { + return &InvalidParentNodeTypeError{ + identifier: id, + parentNodeIdentifier: c.ParentNodeId, + } + } + + child, err := service.nodeStore.Get(c.ChildNodeId) + if err != nil { + return err + } + + var edge composition.Edge + switch c.Type { + case LABEL: + val, ok := c.Configuration.(string) + if !ok { + return &InvalidLabelError{ + identifier: id, + } + } + + edge = composition.NewLabel(val, child) + parentNode.With(edge) + case POSITION: + val, ok := c.Configuration.(int) + if !ok { + return &InvalidPositionError{ + identifier: id, + } + } + + edge = composition.NewPosition(val, child) + parentNode.With(edge) + default: + return &InvalidTypeError{ + kind: string(c.Type), + identifier: id, + } + } + + return service.Store.Create(edge, id) +} diff --git a/samples/krill/components/formatter/errors.go b/samples/krill/components/formatter/errors.go new file mode 100644 index 0000000..4b48c1c --- /dev/null +++ b/samples/krill/components/formatter/errors.go @@ -0,0 +1,21 @@ +package formatter + +import ( + "fmt" + + "github.com/iot-for-all/device-simulation/lib/errors" +) + +type InvalidTypeError struct { + errors.BadRequest + identifier string + kind string +} + +func (err *InvalidTypeError) Error() string { + return fmt.Sprintf( + "attempted to create formatter with id %s of non-existent type %s", + err.identifier, + err.kind, + ) +} diff --git a/samples/krill/components/formatter/formatter.go b/samples/krill/components/formatter/formatter.go new file mode 100644 index 0000000..89ee0e6 --- /dev/null +++ b/samples/krill/components/formatter/formatter.go @@ -0,0 +1,198 @@ +// Package formatter provides the implementation of the formatter component of the simulation framework. +package formatter + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "errors" + "io" + + binaryEncoder "github.com/iot-for-all/device-simulation/lib/binary" + "github.com/iot-for-all/device-simulation/lib/flatten" + protoEncoder "github.com/iot-for-all/device-simulation/lib/proto" + "google.golang.org/protobuf/proto" +) + +var ( + ErrCannotFormatCSV = errors.New("the CSV could not be formatted") + ErrInconsistentCSVColumns = errors.New("each row in the CSV should be the same length") + ErrCannotParseBinary = errors.New("binary payloads cannot be parsed") + ErrInvalidBinaryFormatType = errors.New("cannot format this type into binary") +) + +type Formatter interface { + Format(any) ([]byte, error) + Parse([]byte) (any, error) +} + +type MarshallerUnmarshaller interface { + Marshal(v any) ([]byte, error) + Unmarshal(data []byte, v any) error +} + +type Json struct{} + +func (j *Json) Marshal(v any) ([]byte, error) { + return json.Marshal(v) +} + +func (j *Json) Unmarshal(data []byte, v any) error { + return json.Unmarshal(data, v) +} + +type JsonFormatter struct { + Marshaller MarshallerUnmarshaller +} + +func NewJsonFormatter(options ...func(*JsonFormatter)) *JsonFormatter { + formatter := &JsonFormatter{ + Marshaller: &Json{}, + } + + for _, option := range options { + option(formatter) + } + + return formatter +} + +func (formatter *JsonFormatter) Format(a any) ([]byte, error) { + return formatter.Marshaller.Marshal(a) +} + +func (formatter *JsonFormatter) Parse(b []byte) (any, error) { + var res map[string]any + err := formatter.Marshaller.Unmarshal(b, &res) + return res, err +} + +type BinaryFormatter struct { + encoder binaryEncoder.Encoder +} + +func NewBinaryFormatter(encoder binaryEncoder.Encoder) *BinaryFormatter { + return &BinaryFormatter{ + encoder: encoder, + } +} + +func (formatter *BinaryFormatter) Format(a any) ([]byte, error) { + return formatter.encoder.Encode(a) +} + +func (formatter *BinaryFormatter) Parse([]byte) (any, error) { + return nil, ErrCannotParseBinary +} + +type CSVFormatter struct { + flattener flatten.Flattener + CreateWriter func(w io.Writer) Writer +} + +type Writer interface { + WriteAll(records [][]string) error +} + +func NewCSVFormatter(flattener flatten.Flattener, options ...func(*CSVFormatter)) *CSVFormatter { + formatter := &CSVFormatter{ + flattener: flattener, + CreateWriter: func(w io.Writer) Writer { + return csv.NewWriter(w) + }, + } + + for _, option := range options { + option(formatter) + } + + return formatter +} + +func (formatter *CSVFormatter) Format(a any) ([]byte, error) { + + entries, ok := a.([]any) + if !ok { + return nil, ErrCannotFormatCSV + } + + res := make([][]string, len(entries)+1) + + var headers []string + for idx, entry := range entries { + flattened, err := formatter.flattener.Flatten("csv", entry) + if err != nil { + return nil, err + } + + if idx == 0 { + for _, field := range flattened { + headers = append(headers, field.Key) + } + res[0] = headers + } + + if len(headers) != len(flattened) { + return nil, ErrInconsistentCSVColumns + } + + fields := make([]string, len(flattened)) + for idx, field := range flattened { + fields[idx] = field.Value + } + res[idx+1] = fields + } + + buf := bytes.NewBuffer(nil) + + writer := formatter.CreateWriter(buf) + + err := writer.WriteAll(res) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (formatter *CSVFormatter) Parse(b []byte) (any, error) { + return nil, nil +} + +type ProtobufFormatter struct { + encoder protoEncoder.Encoder +} + +func NewProtobufFormatter(encoder protoEncoder.Encoder) *ProtobufFormatter { + return &ProtobufFormatter{ + encoder: encoder, + } +} + +func (formatter *ProtobufFormatter) Format(a any) ([]byte, error) { + return proto.Marshal(formatter.encoder.Encode(a)) +} + +func (formatter *ProtobufFormatter) Parse(b []byte) (any, error) { + message := new(protoEncoder.Message) + + err := proto.Unmarshal(b, message) + if err != nil { + return nil, err + } + + return formatter.encoder.Decode(message), nil +} + +type MockFormatter struct { + OnFormat func(any) ([]byte, error) + OnParse func([]byte) (any, error) +} + +func (formatter *MockFormatter) Format(a any) ([]byte, error) { + return formatter.OnFormat(a) +} + +func (formatter *MockFormatter) Parse(b []byte) (any, error) { + return formatter.OnParse(b) +} diff --git a/samples/krill/components/formatter/formatter_test.go b/samples/krill/components/formatter/formatter_test.go new file mode 100644 index 0000000..a64f7e3 --- /dev/null +++ b/samples/krill/components/formatter/formatter_test.go @@ -0,0 +1,170 @@ +package formatter + +import ( + "errors" + "io" + "testing" + + "github.com/iot-for-all/device-simulation/lib/binary" + "github.com/iot-for-all/device-simulation/lib/flatten" + "github.com/iot-for-all/device-simulation/lib/proto" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestBinaryFormatter(t *testing.T) { + expected := 1 + formatter := NewBinaryFormatter(&binary.MockEncoder{ + OnEncode: func(a any) ([]byte, error) { + require.Equal(t, expected, a) + return nil, nil + }, + }) + + _, err := formatter.Format(expected) + require.NoError(t, err) + + _, err = formatter.Parse(nil) + require.Equal(t, ErrCannotParseBinary, err) +} + +func TestCSVFormatter(t *testing.T) { + res := []flatten.Field{ + { + Key: "key", + Value: "value", + }, + } + formatter := NewCSVFormatter(&flatten.MockFlattener{ + OnFlatten: func(parent string, entry any) ([]flatten.Field, error) { + return res, nil + }, + }) + + _, err := formatter.Format(1) + require.Equal(t, ErrCannotFormatCSV, err) + + _, err = formatter.Format([]any{1}) + require.NoError(t, err) +} + +func TestCSVFormatterHeaderError(t *testing.T) { + res := []flatten.Field{ + { + Key: "key", + Value: "value", + }, + } + formatter := NewCSVFormatter(&flatten.MockFlattener{ + OnFlatten: func(parent string, entry any) ([]flatten.Field, error) { + r := res + res = []flatten.Field{ + { + Key: "key", + Value: "value", + }, + { + Key: "key-2", + Value: "value", + }, + } + return r, nil + }, + }) + + _, err := formatter.Format([]any{1, 2}) + require.Equal(t, ErrInconsistentCSVColumns, err) +} + +type MockWriter struct { + writeAll func(records [][]string) error +} + +func (writer *MockWriter) WriteAll(records [][]string) error { + return writer.writeAll(records) +} + +func TestCSVFormatterWriterError(t *testing.T) { + res := []flatten.Field{ + { + Key: "key", + Value: "value", + }, + } + + mockErr := errors.New("mock error") + + formatter := NewCSVFormatter(&flatten.MockFlattener{ + OnFlatten: func(parent string, entry any) ([]flatten.Field, error) { + return res, nil + }, + }, func(c *CSVFormatter) { + c.CreateWriter = func(w io.Writer) Writer { + return &MockWriter{ + writeAll: func(records [][]string) error { + return mockErr + }, + } + } + }) + + _, err := formatter.Format([]any{1}) + require.Equal(t, mockErr, err) +} + +func TestProtobufFormatter(t *testing.T) { + formatter := NewProtobufFormatter(&proto.MockEncoder{ + OnEncode: func(a any) *proto.Message { + return &proto.Message{ + Options: &proto.Message_Integer{ + Integer: 1, + }, + } + }, OnDecode: func(m *proto.Message) any { + return 1 + }, + }) + + res, err := formatter.Format(1) + require.NoError(t, err) + + _, err = formatter.Parse(res) + require.NoError(t, err) + + _, err = formatter.Parse([]byte{0, 0}) + require.Error(t, err) +} + +type MockMarshallerUnmarshaller struct { + OnMarshal func(v any) ([]byte, error) + OnUnmarshal func(data []byte, v any) error +} + +func (marshaller *MockMarshallerUnmarshaller) Marshal(v any) ([]byte, error) { + return marshaller.OnMarshal(v) +} + +func (marshaller *MockMarshallerUnmarshaller) Unmarshal(data []byte, v any) error { + return marshaller.OnUnmarshal(data, v) +} + +func TestJsonFormatter(t *testing.T) { + formatter := NewJsonFormatter(func(jf *JsonFormatter) { + jf.Marshaller = &MockMarshallerUnmarshaller{ + OnMarshal: func(v any) ([]byte, error) { + return nil, nil + }, OnUnmarshal: func(data []byte, v any) error { + return nil + }, + } + }) + + _, err := formatter.Format(nil) + require.NoError(t, err) + + _, err = formatter.Parse(nil) + require.NoError(t, err) +} diff --git a/samples/krill/components/formatter/service.go b/samples/krill/components/formatter/service.go new file mode 100644 index 0000000..9948953 --- /dev/null +++ b/samples/krill/components/formatter/service.go @@ -0,0 +1,63 @@ +package formatter + +import ( + "encoding/binary" + + encoder "github.com/iot-for-all/device-simulation/lib/binary" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/flatten" + "github.com/iot-for-all/device-simulation/lib/proto" +) + +type Store component.Store[Formatter, component.ID] + +type Type string + +const ( + JSON Type = "json" + LITTLE_ENDIAN Type = "littleEndian" + BIG_ENDIAN Type = "bigEndian" + CSV Type = "csv" + PROTOBUF Type = "protobuf" +) + +type Component struct { + Type Type +} + +type Service struct { + Store +} + +func NewStore() Store { + return component.New[Formatter, component.ID]() +} + +func NewService(store Store) *Service { + return &Service{ + Store: store, + } +} + +func (service *Service) Create(id component.ID, c *Component) error { + var formatter Formatter + switch c.Type { + case JSON: + formatter = NewJsonFormatter() + case LITTLE_ENDIAN: + formatter = NewBinaryFormatter(encoder.New(binary.LittleEndian)) + case BIG_ENDIAN: + formatter = NewBinaryFormatter(encoder.New(binary.BigEndian)) + case CSV: + formatter = NewCSVFormatter(flatten.New()) + case PROTOBUF: + formatter = NewProtobufFormatter(proto.New()) + default: + return &InvalidTypeError{ + identifier: string(id), + kind: string(c.Type), + } + } + + return service.Store.Create(formatter, id) +} diff --git a/samples/krill/components/formatter/service_test.go b/samples/krill/components/formatter/service_test.go new file mode 100644 index 0000000..d9ebdaa --- /dev/null +++ b/samples/krill/components/formatter/service_test.go @@ -0,0 +1,111 @@ +package formatter + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockInvalidType = "MockInvalidType" +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[Formatter, component.ID]) + require.True(t, ok) +} + +func TestFormatterServiceJSON(t *testing.T) { + service := NewService(&component.MockStore[Formatter, component.ID]{ + OnCreate: func(entity Formatter, identifier component.ID) error { + _, ok := entity.(*JsonFormatter) + require.True(t, ok) + require.Equal(t, MockID, string(identifier)) + return nil + }, + }) + + err := service.Create(MockID, &Component{ + Type: JSON, + }) + require.NoError(t, err) +} + +func TestFormatterServiceLittleEndian(t *testing.T) { + service := NewService(&component.MockStore[Formatter, component.ID]{ + OnCreate: func(entity Formatter, identifier component.ID) error { + _, ok := entity.(*BinaryFormatter) + require.True(t, ok) + require.Equal(t, MockID, string(identifier)) + return nil + }, + }) + + err := service.Create(MockID, &Component{ + Type: LITTLE_ENDIAN, + }) + require.NoError(t, err) +} + +func TestFormatterServiceBigEndian(t *testing.T) { + service := NewService(&component.MockStore[Formatter, component.ID]{ + OnCreate: func(entity Formatter, identifier component.ID) error { + _, ok := entity.(*BinaryFormatter) + require.True(t, ok) + require.Equal(t, MockID, string(identifier)) + return nil + }, + }) + + err := service.Create(MockID, &Component{ + Type: BIG_ENDIAN, + }) + require.NoError(t, err) +} + +func TestFormatterServiceCSV(t *testing.T) { + service := NewService(&component.MockStore[Formatter, component.ID]{ + OnCreate: func(entity Formatter, identifier component.ID) error { + _, ok := entity.(*CSVFormatter) + require.True(t, ok) + require.Equal(t, MockID, string(identifier)) + return nil + }, + }) + + err := service.Create(MockID, &Component{ + Type: CSV, + }) + require.NoError(t, err) +} + +func TestFormatterServiceProtobuf(t *testing.T) { + service := NewService(&component.MockStore[Formatter, component.ID]{ + OnCreate: func(entity Formatter, identifier component.ID) error { + _, ok := entity.(*ProtobufFormatter) + require.True(t, ok) + require.Equal(t, MockID, string(identifier)) + return nil + }, + }) + + err := service.Create(MockID, &Component{ + Type: PROTOBUF, + }) + require.NoError(t, err) +} + +func TestFormatterServiceInvalidType(t *testing.T) { + service := NewService(nil) + + err := service.Create(MockID, &Component{ + Type: MockInvalidType, + }) + require.Equal(t, &InvalidTypeError{ + identifier: MockID, + kind: MockInvalidType, + }, err) +} diff --git a/samples/krill/components/limiter/errors.go b/samples/krill/components/limiter/errors.go new file mode 100644 index 0000000..cf4fc99 --- /dev/null +++ b/samples/krill/components/limiter/errors.go @@ -0,0 +1,25 @@ +package limiter + +import ( + "fmt" + + "github.com/iot-for-all/device-simulation/lib/errors" +) + +type InvalidLimitError struct { + errors.BadRequest + value int +} + +func (err *InvalidLimitError) Error() string { + return fmt.Sprintf("limiter cannot have a limit of less than 1 (provided value %d)", err.value) +} + +type InvalidPeriodSecondsError struct { + errors.BadRequest + value int +} + +func (err *InvalidPeriodSecondsError) Error() string { + return fmt.Sprintf("limiter cannot have a period seconds value of less than 1 (provided value %d)", err.value) +} \ No newline at end of file diff --git a/samples/krill/components/limiter/limiter.go b/samples/krill/components/limiter/limiter.go new file mode 100755 index 0000000..d159791 --- /dev/null +++ b/samples/krill/components/limiter/limiter.go @@ -0,0 +1,121 @@ +package limiter + +import ( + "context" + "time" +) + +type Starter interface { + Start(context.Context) +} + +type Stopper interface { + Stop() +} + +type Limiter[T any] interface { + Starter + Stopper + InputOutput[T] +} + +type InputOutput[T any] interface { + Input() chan<- T + Output() <-chan T +} + +type TimedLimiter[T any] struct { + done chan struct{} + input chan T + output chan T + Limit int + Period time.Duration +} + +func NewTimedLimiter[T any]( + options ...func(*TimedLimiter[T]), +) *TimedLimiter[T] { + limiter := &TimedLimiter[T]{ + input: make(chan T), + output: make(chan T), + done: make(chan struct{}), + Limit: 1, + Period: time.Second, + } + + for _, option := range options { + option(limiter) + } + + return limiter +} + +func (limiter *TimedLimiter[T]) Start(ctx context.Context) { + defer close(limiter.output) + ticker := time.NewTicker(limiter.Period / time.Duration(limiter.Limit)) + defer ticker.Stop() + for { + select { + case <-ticker.C: + next, ok := <-limiter.input + if !ok { + return + } + select { + case limiter.output <- next: + case <-ctx.Done(): + return + } + case <-ctx.Done(): + return + case <-limiter.done: + return + } + } +} + +func (limiter *TimedLimiter[T]) Stop() { + close(limiter.done) +} + +func (limiter *TimedLimiter[T]) Input() chan<- T { + return limiter.input +} + +func (limiter *TimedLimiter[T]) Output() <-chan T { + return limiter.output +} + +type NoopLimiter[T any] struct { + C chan T +} + +func (limiter *NoopLimiter[T]) Start(ctx context.Context) { + <-ctx.Done() +} + +func (limiter *NoopLimiter[T]) Input() chan<- T { + return limiter.C +} + +func (limiter *NoopLimiter[T]) Output() <-chan T { + return limiter.C +} + +func (limiter *NoopLimiter[T]) Stop() {} + +type MockLimiter[T any] struct { + Starter + OnInput func() chan<- T + OnOutput func() <-chan T +} + +func (limiter *MockLimiter[T]) Input() chan<- T { + return limiter.OnInput() +} + +func (limiter *MockLimiter[T]) Output() <-chan T { + return limiter.OnOutput() +} + +func (limiter *MockLimiter[T]) Stop() {} \ No newline at end of file diff --git a/samples/krill/components/limiter/limiter_test.go b/samples/krill/components/limiter/limiter_test.go new file mode 100755 index 0000000..5a2b473 --- /dev/null +++ b/samples/krill/components/limiter/limiter_test.go @@ -0,0 +1,80 @@ +package limiter + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestLimiter(t *testing.T) { + + ctx := context.Background() + + expectedMinimumDurationMs := 4 + reqsPerSecond := 1000 + + limiter := &TimedLimiter[struct{}]{ + Limit: reqsPerSecond, + Period: time.Duration(expectedMinimumDurationMs) * time.Millisecond, + input: make(chan struct{}), + output: make(chan struct{}), + } + + go limiter.Start(ctx) + + done := make(chan struct{}) + + var elapsed time.Duration + + go func() { + start := time.Now() + for count := 0; count < (expectedMinimumDurationMs+1)*reqsPerSecond; count++ { + limiter.Input() <- struct{}{} + <-limiter.Output() + } + elapsed = time.Since(start) + close(done) + }() + + <-done + + require.LessOrEqual( + t, + time.Millisecond*time.Duration(expectedMinimumDurationMs), + elapsed, + ) +} + +func TestLimiterCancellation(t *testing.T) { + + ctx, cancel := context.WithCancel(context.Background()) + + // Enforce extremely slow rate limiting. + limiter := &TimedLimiter[struct{}]{ + Limit: 1, + Period: time.Hour, + input: make(chan struct{}), + output: make(chan struct{}), + } + + go limiter.Start(ctx) + + done := make(chan struct{}) + + go func() { + for count := 0; count < 1000; count++ { + limiter.Input() <- struct{}{} + <-limiter.Output() + } + close(done) + }() + + cancel() + <-limiter.Output() +} \ No newline at end of file diff --git a/samples/krill/components/limiter/service.go b/samples/krill/components/limiter/service.go new file mode 100644 index 0000000..f95e95c --- /dev/null +++ b/samples/krill/components/limiter/service.go @@ -0,0 +1,53 @@ +package limiter + +import ( + "context" + "time" + + "github.com/iot-for-all/device-simulation/lib/component" +) + +type Store component.Store[Limiter[struct{}], component.ID] + +type Component struct { + Limit int + PeriodSeconds int +} + +type Service struct { + Store + ctx context.Context +} + +func NewStore() Store { + return component.New[Limiter[struct{}], component.ID]() +} + +func NewService(ctx context.Context, store Store) *Service { + return &Service{ + Store: store, + ctx: ctx, + } +} + +func (service *Service) Create(id component.ID, c *Component) error { + if c.Limit < 1 { + return &InvalidLimitError{ + value: c.Limit, + } + } + + if c.PeriodSeconds < 1 { + return &InvalidPeriodSecondsError{ + value: c.PeriodSeconds, + } + } + + lim := NewTimedLimiter(func(tl *TimedLimiter[struct{}]) { + tl.Limit = c.Limit + tl.Period = time.Duration(c.PeriodSeconds) * time.Second + }) + go lim.Start(service.ctx) + + return service.Store.Create(lim, id) +} diff --git a/samples/krill/components/limiter/service_test.go b/samples/krill/components/limiter/service_test.go new file mode 100644 index 0000000..e73aa20 --- /dev/null +++ b/samples/krill/components/limiter/service_test.go @@ -0,0 +1,67 @@ +package limiter + +import ( + "context" + "testing" + "time" + + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockLimit = 1 + MockPeriodSeconds = 2 + MockInvalid = 0 +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[Limiter[struct{}], component.ID]) + require.True(t, ok) +} + +func TestLimiterService(t *testing.T) { + + expectedDuration := time.Duration(MockPeriodSeconds) * time.Second + + service := NewService(context.Background(), &component.MockStore[Limiter[struct{}], component.ID]{ + OnCreate: func(entity Limiter[struct{}], identifier component.ID) error { + res, ok := entity.(*TimedLimiter[struct{}]) + require.True(t, ok) + require.Equal(t, MockLimit, res.Limit) + require.Equal(t, expectedDuration, res.Period) + return nil + }, + }) + + err := service.Create(MockID, &Component{ + PeriodSeconds: MockPeriodSeconds, + Limit: MockLimit, + }) + require.NoError(t, err) +} + +func TestLimiterServiceLimitError(t *testing.T) { + service := NewService(context.Background(), nil) + + err := service.Create(MockID, &Component{ + Limit: MockInvalid, + }) + require.Equal(t, &InvalidLimitError{ + value: MockInvalid, + }, err) +} + +func TestLimiterServicePeriodSecondsError(t *testing.T) { + service := NewService(context.Background(), nil) + + err := service.Create(MockID, &Component{ + Limit: MockLimit, + PeriodSeconds: MockInvalid, + }) + require.Equal(t, &InvalidPeriodSecondsError{ + value: MockInvalid, + }, err) +} diff --git a/samples/krill/components/node/errors.go b/samples/krill/components/node/errors.go new file mode 100644 index 0000000..b2af052 --- /dev/null +++ b/samples/krill/components/node/errors.go @@ -0,0 +1,33 @@ +package node + +import ( + "fmt" + + "github.com/iot-for-all/device-simulation/lib/errors" +) + +type InvalidConfigurationError struct { + errors.BadRequest + identifier string +} + +func (err *InvalidConfigurationError) Error() string { + return fmt.Sprintf( + "attempted to created an expression node with identifier %s with a non-string configuration", + err.identifier, + ) +} + +type InvalidTypeError struct { + errors.BadRequest + kind string + identifier string +} + +func (err *InvalidTypeError) Error() string { + return fmt.Sprintf( + "attempted to create a node (identifier %s) with an invalid node type %s", + err.identifier, + err.kind, + ) +} \ No newline at end of file diff --git a/samples/krill/components/node/node_test.go b/samples/krill/components/node/node_test.go new file mode 100644 index 0000000..5c894a2 --- /dev/null +++ b/samples/krill/components/node/node_test.go @@ -0,0 +1,101 @@ +package node + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/composition" + "github.com/iot-for-all/device-simulation/lib/logger" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockInvalidType = "MockInvalidType" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[composition.Renderer, component.ID]) + require.True(t, ok) +} + +func TestServiceExpression(t *testing.T) { + service := NewService(&component.MockStore[composition.Renderer, component.ID]{ + OnCreate: func(entity composition.Renderer, identifier component.ID) error { + _, ok := entity.(*composition.Expression) + require.True(t, ok) + return nil + }, + }, func(s *Service) { + s.Logger = &logger.NoopLogger{} + }) + + err := service.Create(MockID, &Component{ + Type: EXPRESSION, + Configuration: "x", + }) + require.NoError(t, err) +} + +func TestServiceCollection(t *testing.T) { + service := NewService(&component.MockStore[composition.Renderer, component.ID]{ + OnCreate: func(entity composition.Renderer, identifier component.ID) error { + _, ok := entity.(*composition.Collection) + require.True(t, ok) + return nil + }, + }) + + err := service.Create(MockID, &Component{ + Type: COLLECTION, + }) + require.NoError(t, err) +} + +func TestServiceArray(t *testing.T) { + service := NewService(&component.MockStore[composition.Renderer, component.ID]{ + OnCreate: func(entity composition.Renderer, identifier component.ID) error { + _, ok := entity.(*composition.Array) + require.True(t, ok) + return nil + }, + }) + + err := service.Create(MockID, &Component{ + Type: ARRAY, + }) + require.NoError(t, err) +} + +func TestServiceTypeError(t *testing.T) { + service := NewService(&component.MockStore[composition.Renderer, component.ID]{ + OnCreate: func(entity composition.Renderer, identifier component.ID) error { + _, ok := entity.(*composition.Array) + require.True(t, ok) + return nil + }, + }) + + err := service.Create(MockID, &Component{ + Type: MockInvalidType, + }) + require.Equal(t, &InvalidTypeError{ + kind: MockInvalidType, + identifier: MockID, + }, err) +} + +func TestServiceExpressionParseError(t *testing.T) { + service := NewService(nil) + + err := service.Create(MockID, &Component{ + Type: EXPRESSION, + Configuration: "", + }) + require.Error(t, err) +} diff --git a/samples/krill/components/node/service.go b/samples/krill/components/node/service.go new file mode 100644 index 0000000..c754951 --- /dev/null +++ b/samples/krill/components/node/service.go @@ -0,0 +1,74 @@ +package node + +import ( + "go/parser" + + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/composition" + "github.com/iot-for-all/device-simulation/lib/expression" + "github.com/iot-for-all/device-simulation/lib/logger" +) + +type Store component.Store[composition.Renderer, component.ID] + +type Type string + +const ( + EXPRESSION Type = "expression" + COLLECTION Type = "collection" + ARRAY Type = "array" +) + +type Component struct { + Type Type + Configuration string +} + +type Service struct { + Store + Logger logger.Logger +} + +func NewStore() Store { + return component.New[composition.Renderer, component.ID]() +} + +func NewService(store Store, options ...func(*Service)) *Service { + service := &Service{ + Store: store, + Logger: &logger.NoopLogger{}, + } + + for _, option := range options { + option(service) + } + + return service +} + +func (service *Service) Create(id component.ID, c *Component) error { + + var node composition.Renderer + switch c.Type { + case EXPRESSION: + psr, err := parser.ParseExpr(c.Configuration) + if err != nil { + return err + } + + node = composition.NewExpression(expression.New(psr), func(e *composition.Expression) { + e.Logger = service.Logger + }) + case COLLECTION: + node = composition.NewCollection() + case ARRAY: + node = composition.NewArray() + default: + return &InvalidTypeError{ + kind: string(c.Type), + identifier: string(id), + } + } + + return service.Store.Create(node, id) +} diff --git a/samples/krill/components/observer/observer.go b/samples/krill/components/observer/observer.go new file mode 100644 index 0000000..27dbe33 --- /dev/null +++ b/samples/krill/components/observer/observer.go @@ -0,0 +1,56 @@ +package observer + +import ( + "github.com/iot-for-all/device-simulation/components/registry" +) + +// Observer is an implementation of the CancellableObservable interface and serves as the observer component in the simulation framework. +type Observer struct { + observable registry.CancellableObservable + ID int + registry registry.Registry +} + +// NewObserver creates an observer given an observable and a registry. +// It will register the observer with the registry when called. +func NewObserver( + observable registry.CancellableObservable, + registry registry.Registry, +) *Observer { + observer := &Observer{ + observable: observable, + registry: registry, + } + + observer.ID = observer.registry.Register(observer) + + return observer +} + +// Observe will call the observable's observe function, passing through its observed value. +func (observer *Observer) Observe(val float64) { + observer.observable.Observe(val) +} + +// Cancel will deregister the observable from the registry and then cancel the observable. +func (observer *Observer) Cancel() { + observer.registry.Deregister(observer.ID) + observer.observable.Cancel() +} + +type NoopObservable struct{} + +func (*NoopObservable) Observe(val float64) {} + +type MockObserver struct { + OnObserve func(val float64) + OnCancel func() +} + +func (obs *MockObserver) Observe(val float64) { + obs.OnObserve(val) +} + +func (obs *MockObserver) Cancel() { + obs.OnCancel() +} \ No newline at end of file diff --git a/samples/krill/components/observer/observer_test.go b/samples/krill/components/observer/observer_test.go new file mode 100644 index 0000000..7f0c7bd --- /dev/null +++ b/samples/krill/components/observer/observer_test.go @@ -0,0 +1,78 @@ +package observer + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestMockObserver(t *testing.T) { + expected := 101.0 + cancelled := make(chan struct{}) + obs := &MockObserver{ + OnObserve: func(val float64) { + require.Equal(t, expected, val) + }, OnCancel: func() { + close(cancelled) + }, + } + + obs.Observe(expected) + go obs.Cancel() + <-cancelled +} + +func TestBasicRegistryAndObserver(t *testing.T) { + + expected := 101.0 + reg := registry.NewRegistry() + cancelled := make(chan struct{}) + observed := make(chan struct{}) + obs := NewObserver(&MockObserver{ + OnObserve: func(val float64) { + require.Equal(t, expected, val) + close(observed) + }, OnCancel: func() { + close(cancelled) + }, + }, reg) + + go reg.Observe(expected) + <-observed + go obs.Cancel() + <-cancelled +} + +func TestRegistryWithMultipleObservables(t *testing.T) { + expected := 101.0 + reg := registry.NewRegistry() + cancelled := make(chan struct{}) + observed := make(chan struct{}) + obsOne := NewObserver(&MockObserver{ + OnObserve: func(val float64) { + require.Equal(t, expected, val) + observed <- struct{}{} + }, OnCancel: func() { + close(cancelled) + }, + }, reg) + NewObserver(&MockObserver{ + OnObserve: func(val float64) { + require.Equal(t, expected, val) + observed <- struct{}{} + }, + }, reg) + + go reg.Observe(expected) + <-observed + <-observed + close(observed) + + go obsOne.Cancel() + <-cancelled +} \ No newline at end of file diff --git a/samples/krill/components/observer/service.go b/samples/krill/components/observer/service.go new file mode 100644 index 0000000..2ae9576 --- /dev/null +++ b/samples/krill/components/observer/service.go @@ -0,0 +1,52 @@ +package observer + +import ( + "github.com/iot-for-all/device-simulation/components/provider" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" +) + +type Store component.Store[*Observer, component.ID] + +type Component struct { + RegistryID component.ID + ProviderID component.ID + Label string +} + +type Service struct { + Store + registryStore registry.Store + providerStore provider.Store +} + +func NewStore() Store { + return component.New[*Observer, component.ID]() +} + +func NewService(store Store, registryStore registry.Store, providerStore provider.Store) *Service { + return &Service{ + Store: store, + registryStore: registryStore, + providerStore: providerStore, + } +} + +func (service *Service) Create(id component.ID, c *Component) error { + reg, err := service.registryStore.Get(c.RegistryID) + if err != nil { + return err + } + + prov, err := service.providerStore.Get(c.ProviderID) + if err != nil { + return err + } + + obs, err := prov.With(c.Label) + if err != nil { + return err + } + + return service.Store.Create(NewObserver(obs, reg), id) +} diff --git a/samples/krill/components/observer/service_test.go b/samples/krill/components/observer/service_test.go new file mode 100644 index 0000000..6bff811 --- /dev/null +++ b/samples/krill/components/observer/service_test.go @@ -0,0 +1,116 @@ +package observer + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/provider" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockObserverID = 1 + MockLabel = "MockLabel" + MockRegistryID = "MockRegistryID" + MockProviderID = "MockProviderID" +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[*Observer, component.ID]) + require.True(t, ok) +} + +func TestObserverService(t *testing.T) { + service := NewService(&component.MockStore[*Observer, component.ID]{ + OnCreate: func(entity *Observer, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + _, ok := entity.registry.(*registry.MockRegistry) + require.True(t, ok) + _, ok = entity.observable.(*registry.MockObservable) + require.True(t, ok) + require.Equal(t, MockObserverID, entity.ID) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return ®istry.MockRegistry{ + OnRegister: func(o registry.Observable) int { + res, ok := o.(*Observer) + require.True(t, ok) + _, ok = res.registry.(*registry.MockRegistry) + require.True(t, ok) + _, ok = res.observable.(*registry.MockObservable) + require.True(t, ok) + + return MockObserverID + }, + }, nil + }, + }, &component.MockStore[provider.Provider, component.ID]{ + OnGet: func(identifier component.ID) (provider.Provider, error) { + require.Equal(t, MockProviderID, string(identifier)) + return &provider.MockProvider{ + OnWith: func(label string) (registry.CancellableObservable, error) { + require.Equal(t, MockLabel, label) + return ®istry.MockObservable{}, nil + }, + }, nil + }, + }) + + err := service.Create(MockID, &Component{ + RegistryID: MockRegistryID, + ProviderID: MockProviderID, + Label: MockLabel, + }) + require.NoError(t, err) +} + +func TestObserverServiceRegistryStoreError(t *testing.T) { + service := NewService(nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.MockError{} + }, + }, nil) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} + +func TestObserverServiceProviderStoreError(t *testing.T) { + service := NewService(nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }, &component.MockStore[provider.Provider, component.ID]{ + OnGet: func(identifier component.ID) (provider.Provider, error) { + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} + +func TestObserverServiceProviderWithError(t *testing.T) { + service := NewService(nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }, &component.MockStore[provider.Provider, component.ID]{ + OnGet: func(identifier component.ID) (provider.Provider, error) { + return &provider.MockProvider{ + OnWith: func(label string) (registry.CancellableObservable, error) { + return nil, &component.MockError{} + }, + }, nil + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} diff --git a/samples/krill/components/outlet/outlet.go b/samples/krill/components/outlet/outlet.go new file mode 100644 index 0000000..035abf1 --- /dev/null +++ b/samples/krill/components/outlet/outlet.go @@ -0,0 +1,70 @@ +package outlet + +import ( + "errors" + + "github.com/iot-for-all/device-simulation/lib/expression" + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/components/registry" +) + +var ( + ErrInvalidParsedResultType = errors.New("the observed message body must be parsed into a map of string to any") + ErrInvalidObservationType = errors.New("only floats or integer values can be observed") +) + +type Outlet interface { + Observe([]byte) error +} + +type PrometheusOutlet struct { + expression expression.Evaluator + formatter formatter.Formatter + monitor registry.Observable +} + +func NewPrometheusOutlet( + expr expression.Evaluator, + frmt formatter.Formatter, + monitor registry.Observable, +) *PrometheusOutlet { + return &PrometheusOutlet{ + expression: expr, + formatter: frmt, + monitor: monitor, + } +} + +func (outlet *PrometheusOutlet) Observe(b []byte) error { + res, err := outlet.formatter.Parse(b) + if err != nil { + return err + } + + env, ok := res.(map[string]any) + if !ok { + return ErrInvalidParsedResultType + } + + val, err := outlet.expression.Evaluate(env) + if err != nil { + return err + } + + switch observed := val.(type) { + case float64: + outlet.monitor.Observe(observed) + case int: + outlet.monitor.Observe(float64(observed)) + default: + return ErrInvalidObservationType + } + + return nil +} + +type NoopOutlet struct{} + +func (outlet *NoopOutlet) Observe([]byte) error { + return nil +} diff --git a/samples/krill/components/outlet/outlet_test.go b/samples/krill/components/outlet/outlet_test.go new file mode 100644 index 0000000..a5965b7 --- /dev/null +++ b/samples/krill/components/outlet/outlet_test.go @@ -0,0 +1,85 @@ +package outlet + +import ( + "errors" + "testing" + + "github.com/iot-for-all/device-simulation/lib/expression" + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/stretchr/testify/require" +) + +var ( + ErrMock = errors.New("mock error") +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestPrometheusOutlet(t *testing.T) { + expected := 1.0 + outlet := NewPrometheusOutlet(&expression.MockEvaluator{ + OnEvaluate: func(m map[string]any) (any, error) { + return expected, nil + }, + }, &formatter.MockFormatter{ + OnParse: func(b []byte) (any, error) { + return map[string]any{"": expected}, nil + }, + }, ®istry.MockObservable{ + OnObserve: func(val float64) { + require.Equal(t, expected, val) + }, + }) + + err := outlet.Observe(nil) + require.NoError(t, err) +} + +func TestPrometheusOutletIntCast(t *testing.T) { + expected := 1 + outlet := NewPrometheusOutlet(&expression.MockEvaluator{ + OnEvaluate: func(m map[string]any) (any, error) { + return expected, nil + }, + }, &formatter.MockFormatter{ + OnParse: func(b []byte) (any, error) { + return map[string]any{"": expected}, nil + }, + }, ®istry.MockObservable{ + OnObserve: func(val float64) { + require.Equal(t, 1.0, val) + }, + }) + + err := outlet.Observe(nil) + require.NoError(t, err) +} + +func TestPrometheusOutletFormatterError(t *testing.T) { + outlet := NewPrometheusOutlet(&expression.MockEvaluator{}, &formatter.MockFormatter{ + OnParse: func(b []byte) (any, error) { + return nil, ErrMock + }, + }, ®istry.MockObservable{}) + + err := outlet.Observe(nil) + require.Equal(t, ErrMock, err) +} + +func TestPrometheusOutletEvaluatorError(t *testing.T) { + outlet := NewPrometheusOutlet(&expression.MockEvaluator{ + OnEvaluate: func(m map[string]any) (any, error) { + return nil, ErrMock + }, + }, &formatter.MockFormatter{ + OnParse: func(b []byte) (any, error) { + return map[string]any{"": 0}, nil + }, + }, ®istry.MockObservable{}) + + err := outlet.Observe(nil) + require.Equal(t, ErrMock, err) +} diff --git a/samples/krill/components/outlet/service.go b/samples/krill/components/outlet/service.go new file mode 100644 index 0000000..7934438 --- /dev/null +++ b/samples/krill/components/outlet/service.go @@ -0,0 +1,62 @@ +package outlet + +import ( + "go/parser" + + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/expression" +) + +type Store component.Store[Outlet, component.ID] + +type Type string + +type Component struct { + RegistryID component.ID + FormatterID component.ID + Type Type + Configuration string +} + +type Service struct { + Store + formatterStore formatter.Store + registryStore registry.Store +} + +func NewStore() Store { + return component.New[Outlet, component.ID]() +} + +func NewService( + store Store, + formatterStore formatter.Store, + registryStore registry.Store, +) *Service { + return &Service{ + Store: store, + formatterStore: formatterStore, + registryStore: registryStore, + } +} + +func (service *Service) Create(id component.ID, c *Component) error { + reg, err := service.registryStore.Get(c.RegistryID) + if err != nil { + return err + } + + fmtr, err := service.formatterStore.Get(c.FormatterID) + if err != nil { + return err + } + + psr, err := parser.ParseExpr(c.Configuration) + if err != nil { + return err + } + + return service.Store.Create(NewPrometheusOutlet(expression.New(psr), fmtr, reg), id) +} diff --git a/samples/krill/components/outlet/service_test.go b/samples/krill/components/outlet/service_test.go new file mode 100644 index 0000000..3428007 --- /dev/null +++ b/samples/krill/components/outlet/service_test.go @@ -0,0 +1,92 @@ +package outlet + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockRegistryID = "MockRegistryID" + MockFormatterID = "MockFormatterID" + MockConfiguration = "MockConfiguration" +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[Outlet, component.ID]) + require.True(t, ok) +} + +func TestService(t *testing.T) { + service := NewService(&component.MockStore[Outlet, component.ID]{ + OnCreate: func(entity Outlet, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + _, ok := entity.(*PrometheusOutlet) + require.True(t, ok) + return nil + }, + }, &component.MockStore[formatter.Formatter, component.ID]{ + OnGet: func(identifier component.ID) (formatter.Formatter, error) { + require.Equal(t, MockFormatterID, string(identifier)) + return nil, nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{ + RegistryID: MockRegistryID, + FormatterID: MockFormatterID, + Configuration: "MockConfiguration", + }) + require.NoError(t, err) +} + +func TestServiceFormatterError(t *testing.T) { + service := NewService(nil, &component.MockStore[formatter.Formatter, component.ID]{ + OnGet: func(identifier component.ID) (formatter.Formatter, error) { + return nil, &component.MockError{} + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} + +func TestServiceRegistryError(t *testing.T) { + service := NewService(nil, nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} + +func TestServiceParserError(t *testing.T) { + service := NewService(nil, &component.MockStore[formatter.Formatter, component.ID]{ + OnGet: func(identifier component.ID) (formatter.Formatter, error) { + return nil, nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{}) + require.Error(t, err) +} diff --git a/samples/krill/components/provider/errors.go b/samples/krill/components/provider/errors.go new file mode 100644 index 0000000..229e875 --- /dev/null +++ b/samples/krill/components/provider/errors.go @@ -0,0 +1,21 @@ +package provider + +import ( + "fmt" + + "github.com/iot-for-all/device-simulation/lib/errors" +) + +type InvalidTypeError struct { + errors.BadRequest + identifier string + kind string +} + +func (err *InvalidTypeError) Error() string { + return fmt.Sprintf( + "could not create the provider component with id %s because an invalid provider type of %s was given", + err.identifier, + err.kind, + ) +} \ No newline at end of file diff --git a/samples/krill/components/provider/provider.go b/samples/krill/components/provider/provider.go new file mode 100644 index 0000000..dfec7f5 --- /dev/null +++ b/samples/krill/components/provider/provider.go @@ -0,0 +1,47 @@ +package provider + +import ( + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/prometheus/client_golang/prometheus" +) + +// Provider is the interface describing the provider component. +// Its implementations should be able to create a CancellableObservable implementation if provided with a label. +type Provider interface { + With(label string) (registry.CancellableObservable, error) + Cancel() error +} + +type MockProvider struct { + OnWith func(label string) (registry.CancellableObservable, error) + OnCancel func() error +} + +func (provider *MockProvider) With( + label string, +) (registry.CancellableObservable, error) { + return provider.OnWith(label) +} + +func (provider *MockProvider) Cancel() error { + return provider.OnCancel() +} + +type MockRegistry struct { + prometheus.Registerer + OnRegister func(prometheus.Collector) error + OnMustRegister func(...prometheus.Collector) + OnUnregister func(prometheus.Collector) bool +} + +func (reg *MockRegistry) Register(c prometheus.Collector) error { + return reg.OnRegister(c) +} + +func (reg *MockRegistry) MustRegister(c ...prometheus.Collector) { + reg.OnMustRegister(c...) +} + +func (reg *MockRegistry) Unregister(c prometheus.Collector) bool { + return reg.OnUnregister(c) +} diff --git a/samples/krill/components/provider/provider_test.go b/samples/krill/components/provider/provider_test.go new file mode 100644 index 0000000..435763b --- /dev/null +++ b/samples/krill/components/provider/provider_test.go @@ -0,0 +1,31 @@ +package provider + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestMockProvider(t *testing.T) { + expected := "label" + cancelled := make(chan struct{}) + mock := &MockProvider{ + OnWith: func(label string) (registry.CancellableObservable, error) { + require.Equal(t, expected, label) + return nil, nil + }, OnCancel: func() error { + go close(cancelled) + return nil + }, + } + _, err := mock.With(expected) + require.NoError(t, err) + err = mock.Cancel() + require.NoError(t, err) + <-cancelled +} \ No newline at end of file diff --git a/samples/krill/components/provider/service.go b/samples/krill/components/provider/service.go new file mode 100644 index 0000000..9d57ec7 --- /dev/null +++ b/samples/krill/components/provider/service.go @@ -0,0 +1,110 @@ +package provider + +import ( + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/counter" + "github.com/iot-for-all/device-simulation/lib/exporter" + "github.com/iot-for-all/device-simulation/lib/gauge" + "github.com/iot-for-all/device-simulation/lib/histogram" + "github.com/iot-for-all/device-simulation/lib/logger" + "github.com/prometheus/client_golang/prometheus" +) + +type Store component.Store[Provider, component.ID] + +type Type string + +const ( + COUNTER Type = "counter" + HISTOGRAM Type = "histogram" + GAUGE Type = "gauge" + CUSTOM_HISTOGRAM Type = "custom_histogram" +) + +type Component struct { + Help string + Name string + Label string + Start float64 + Width float64 + Buckets int + Type Type +} + +type Service struct { + Store + registry prometheus.Registerer + exporter exporter.Exporter + Logger logger.Logger +} + +func NewStore() Store { + return component.New[Provider, component.ID]() +} + +func NewService(store Store, registry prometheus.Registerer, exp exporter.Exporter, options ...func(*Service)) *Service { + service := &Service{ + Store: store, + registry: registry, + exporter: exp, + Logger: &logger.NoopLogger{}, + } + + for _, option := range options { + option(service) + } + + return service +} + +func (service *Service) Create(id component.ID, c *Component) error { + var provider Provider + var err error + switch c.Type { + case COUNTER: + provider, err = counter.New( + service.registry, + func(cp *counter.Provider) { + cp.Name = c.Name + cp.Help = c.Help + }, + ) + case HISTOGRAM: + provider, err = histogram.New( + service.registry, + func(hp *histogram.Provider) { + hp.Name = c.Name + hp.Help = c.Help + hp.Buckets = c.Buckets + hp.Start = c.Start + hp.Width = c.Width + }, + ) + case GAUGE: + provider, err = gauge.New( + service.registry, + func(gp *gauge.Provider) { + gp.Name = c.Name + gp.Help = c.Help + }, + ) + case CUSTOM_HISTOGRAM: + provider, err = exporter.New(service.exporter, func(chp *exporter.CustomHistogramProvider) { + chp.Name = c.Name + chp.Help = c.Help + chp.Start = int(c.Start) + chp.Width = int(c.Width) + chp.Logger = service.Logger + }) + default: + return &InvalidTypeError{ + identifier: string(id), + kind: string(c.Type), + } + } + if err != nil { + return err + } + + return service.Store.Create(provider, id) +} diff --git a/samples/krill/components/provider/service_test.go b/samples/krill/components/provider/service_test.go new file mode 100644 index 0000000..d2df39b --- /dev/null +++ b/samples/krill/components/provider/service_test.go @@ -0,0 +1,192 @@ +package provider + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/counter" + "github.com/iot-for-all/device-simulation/lib/exporter" + "github.com/iot-for-all/device-simulation/lib/gauge" + "github.com/iot-for-all/device-simulation/lib/histogram" + "github.com/iot-for-all/device-simulation/lib/logger" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockHelp = "MockHelp" + MockName = "MockName" + MockLabel = "MockLabel" + MockInvalidType = "MockInvalidType" + MockStart = 1.0 + MockWidth = 10.0 + MockBuckets = 11 +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[Provider, component.ID]) + require.True(t, ok) +} + +func TestProviderServiceCounter(t *testing.T) { + service := NewService(&component.MockStore[Provider, component.ID]{ + OnCreate: func(entity Provider, identifier component.ID) error { + res, ok := entity.(*counter.Provider) + require.True(t, ok) + require.Equal(t, MockID, string(identifier)) + require.Equal(t, MockHelp, res.Help) + require.Equal(t, MockName, res.Name) + return nil + }, + }, &MockRegistry{ + OnRegister: func(c prometheus.Collector) error { + return nil + }, + }, &exporter.MockExporter{ + OnRegisterHistogram: func(name, help string, start, width int) (exporter.Provider, error) { + return nil, nil + }, + }, func(s *Service) { + s.Logger = &logger.NoopLogger{} + }) + + err := service.Create(MockID, &Component{ + Help: MockHelp, + Name: MockName, + Label: MockLabel, + Start: MockStart, + Width: MockWidth, + Buckets: MockBuckets, + Type: COUNTER, + }) + require.NoError(t, err) +} + +func TestProviderServiceHistogram(t *testing.T) { + service := NewService(&component.MockStore[Provider, component.ID]{ + OnCreate: func(entity Provider, identifier component.ID) error { + res, ok := entity.(*histogram.Provider) + require.True(t, ok) + require.Equal(t, MockID, string(identifier)) + require.Equal(t, MockHelp, res.Help) + require.Equal(t, MockName, res.Name) + require.Equal(t, MockBuckets, res.Buckets) + require.Equal(t, MockStart, res.Start) + require.Equal(t, MockWidth, res.Width) + return nil + }, + }, &MockRegistry{ + OnRegister: func(c prometheus.Collector) error { + return nil + }, + }, nil) + + err := service.Create(MockID, &Component{ + Help: MockHelp, + Name: MockName, + Label: MockLabel, + Start: MockStart, + Width: MockWidth, + Buckets: MockBuckets, + Type: HISTOGRAM, + }) + require.NoError(t, err) +} + +func TestProviderServiceGauge(t *testing.T) { + service := NewService(&component.MockStore[Provider, component.ID]{ + OnCreate: func(entity Provider, identifier component.ID) error { + res, ok := entity.(*gauge.Provider) + require.True(t, ok) + require.Equal(t, MockID, string(identifier)) + require.Equal(t, MockHelp, res.Help) + require.Equal(t, MockName, res.Name) + return nil + }, + }, &MockRegistry{ + OnRegister: func(c prometheus.Collector) error { + return nil + }, + }, nil) + + err := service.Create(MockID, &Component{ + Help: MockHelp, + Name: MockName, + Label: MockLabel, + Type: GAUGE, + }) + require.NoError(t, err) +} + +func TestProviderServiceCustomHistogram(t *testing.T) { + service := NewService(&component.MockStore[Provider, component.ID]{ + OnCreate: func(entity Provider, identifier component.ID) error { + res, ok := entity.(*exporter.CustomHistogramProvider) + require.True(t, ok) + require.Equal(t, MockID, string(identifier)) + require.Equal(t, MockHelp, res.Help) + require.Equal(t, MockName, res.Name) + require.Equal(t, int(MockStart), res.Start) + require.Equal(t, int(MockWidth), res.Width) + return nil + }, + }, nil, &exporter.MockExporter{ + OnRegisterHistogram: func(name, help string, start, width int) (exporter.Provider, error) { + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{ + Help: MockHelp, + Name: MockName, + Label: MockLabel, + Start: MockStart, + Width: MockWidth, + Buckets: MockBuckets, + Type: CUSTOM_HISTOGRAM, + }) + require.NoError(t, err) +} + +func TestProviderServiceCustomHistogramError(t *testing.T) { + service := NewService(&component.MockStore[Provider, component.ID]{ + OnCreate: func(entity Provider, identifier component.ID) error { + res, ok := entity.(*exporter.CustomHistogramProvider) + require.True(t, ok) + require.Equal(t, MockID, string(identifier)) + require.Equal(t, MockHelp, res.Help) + require.Equal(t, MockName, res.Name) + require.Equal(t, int(MockStart), res.Start) + require.Equal(t, int(MockWidth), res.Width) + return nil + }, + }, nil, &exporter.MockExporter{ + OnRegisterHistogram: func(name, help string, start, width int) (exporter.Provider, error) { + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{ + Help: MockHelp, + Name: MockName, + Label: MockLabel, + Start: MockStart, + Width: MockWidth, + Type: CUSTOM_HISTOGRAM, + }) + require.Equal(t, &component.MockError{}, err) +} + +func TestProviderServiceInvalidTypeError(t *testing.T) { + service := NewService(nil, nil, nil) + + err := service.Create(MockID, &Component{ + Type: MockInvalidType, + }) + require.Equal(t, &InvalidTypeError{ + identifier: MockID, + kind: MockInvalidType, + }, err) +} diff --git a/samples/krill/components/publisher/publisher.go b/samples/krill/components/publisher/publisher.go new file mode 100644 index 0000000..9f3fa89 --- /dev/null +++ b/samples/krill/components/publisher/publisher.go @@ -0,0 +1,148 @@ +// Package publisher provides the implementation for the publisher component of the simulation framework along with all associated interfaces. +package publisher + +import ( + "context" + "time" + + "github.com/iot-for-all/device-simulation/components/client" + "github.com/iot-for-all/device-simulation/components/limiter" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/renderer" + "github.com/iot-for-all/device-simulation/components/topic" + "github.com/iot-for-all/device-simulation/components/tracer" + "github.com/iot-for-all/device-simulation/lib/environment" + "github.com/iot-for-all/device-simulation/lib/logger" +) + +// Publisher is a component which routinely publishes messages on a provided topic name. +// It uses a work pool to publish messages. +type Publisher struct { + ctx context.Context + tracer tracer.Tracer + client client.Publisher + renderer renderer.Renderer + topic topic.Renderer + monitor registry.Observable + env environment.Environment + Cancel context.CancelFunc + limiter limiter.Limiter[struct{}] + Logger logger.Logger + QoS int + RendersPerPublish int + MessagesRetained bool + Name string + Site string +} + +// New creates a Publisher given a context, a work pool of type PublishResult, +// a payload to render MQTT message bodies from, a client whose connection will be used to publish messages, +// a topic to render MQTT topic to publish on, and a monitor to observe message publishing latencies with. +// Optional parameters can be set with the options function. +func New( + ctx context.Context, + ren renderer.Renderer, + cli client.Publisher, + top topic.Renderer, + env environment.Environment, + mon registry.Observable, + tra tracer.Tracer, + lim limiter.Limiter[struct{}], + options ...func(*Publisher), +) *Publisher { + publisher := &Publisher{ + client: cli, + topic: top, + renderer: ren, + ctx: ctx, + monitor: mon, + env: env, + limiter: lim, + tracer: tra, + Logger: &logger.NoopLogger{}, + RendersPerPublish: 1, + } + + for _, option := range options { + option(publisher) + } + + publisher.env.Set("x", -1) + publisher.env.Set("start", time.Now()) + publisher.env.Set("site", publisher.Site) + publisher.env.Set("id", publisher.Name) + + ctx, cancel := context.WithCancel(publisher.ctx) + publisher.Cancel = cancel + publisher.ctx = ctx + + return publisher +} + +func (publisher *Publisher) publish(ctx context.Context) error { + + data, err := publisher.renderer.Render( + publisher.env, + publisher.env.Env()["x"].(int)+1, + publisher.RendersPerPublish, + ) + if err != nil { + return err + } + + start := time.Now() + + res := make(chan error) + block := publisher.tracer.Begin() + go func() { + res <- publisher.client.Publish( + publisher.topic.Render(), + byte(publisher.QoS), + publisher.MessagesRetained, + data, + ) + }() + + select { + case <-ctx.Done(): + return nil + case err := <-res: + acknowledgementLatency := time.Since(start) + + publisher.monitor.Observe( + float64(acknowledgementLatency.Milliseconds()), + ) + publisher.topic.Observe(float64(acknowledgementLatency.Milliseconds())) + publisher.client.Observe(float64(acknowledgementLatency.Milliseconds())) + + <-block + + return err + } +} + +// Start will begin the operation of the publisher, only starting to send messages once the underlying client is successfully connected to an MQTT broker. +// It will return if its context is cancelled, or if the underlying client is disconnected. +func (publisher *Publisher) Start() { + <-publisher.client.Connected() + input := publisher.limiter.Input() + output := publisher.limiter.Output() + + errorLvl := publisher.Logger.Level(logger.Error) + + for { + select { + case <-publisher.ctx.Done(): + return + case <-publisher.client.Disconnected(): + return + case input <- struct{}{}: + case <-output: + err := publisher.publish(publisher.ctx) + if err != nil { + errorLvl.With("error", err.Error()). + Printf("error publishing message") + } + } + } +} \ No newline at end of file diff --git a/samples/krill/components/publisher/publisher_test.go b/samples/krill/components/publisher/publisher_test.go new file mode 100644 index 0000000..8d7c551 --- /dev/null +++ b/samples/krill/components/publisher/publisher_test.go @@ -0,0 +1,156 @@ +package publisher + +import ( + "context" + "testing" + + "github.com/iot-for-all/device-simulation/components/client" + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/components/limiter" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/renderer" + "github.com/iot-for-all/device-simulation/components/topic" + "github.com/iot-for-all/device-simulation/components/tracer" + "github.com/iot-for-all/device-simulation/lib/composition" + "github.com/iot-for-all/device-simulation/lib/environment" + "github.com/iot-for-all/device-simulation/lib/errors" + "github.com/iot-for-all/device-simulation/lib/logger" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestPublisher(t *testing.T) { + + ctx := context.Background() + + lim := &limiter.NoopLimiter[struct{}]{ + C: make(chan struct{}, 1), + } + + reg := registry.NewRegistry() + + ren := renderer.New(composition.NewCollection(), formatter.NewJsonFormatter()) + + top := topic.New(reg) + + finished := make(chan struct{}) + + cli := &client.MockClient{ + OnConnect: make(chan struct{}), + OnPublish: func(topic string, qos byte, messagesRetained bool, data []byte) error { + finished <- struct{}{} + return nil + }, Observable: reg, + } + go close(cli.OnConnect) + + env := environment.New() + + publisher := New(ctx, ren, cli, top, env, reg, tracer.NewNoopTracer(), lim, func(p *Publisher) { + p.QoS = 1 + }) + go publisher.Start() + <-finished +} + +func TestPublisherWithPublishError(t *testing.T) { + + ctx := context.Background() + + lim := &limiter.NoopLimiter[struct{}]{ + C: make(chan struct{}, 1), + } + + reg := registry.NewRegistry() + + ren := renderer.New(composition.NewCollection(), formatter.NewJsonFormatter()) + top := topic.New(reg) + cli := &client.MockClient{ + OnConnect: make(chan struct{}), + OnPublish: func(topic string, qos byte, messagesRetained bool, data []byte) error { + return errors.Mock{} + }, Observable: reg, + } + go close(cli.OnConnect) + + finished := make(chan struct{}) + + lg := &logger.MockLogger{ + OnLevel: func(i int) logger.Logger { + return &logger.MockLogger{ + OnWith: func(s1, s2 string) logger.Logger { + require.Equal(t, errors.Mock{}.Error(), s2) + finished <- struct{}{} + return &logger.NoopLogger{} + }, + } + }, + } + + env := environment.New() + + publisher := New(ctx, ren, cli, top, env, reg, tracer.NewNoopTracer(), lim, func(p *Publisher) { + p.QoS = 1 + p.Logger = lg + }) + go publisher.Start() + <-finished +} + +func TestPublisherDisconnectViaClientDisconnect(t *testing.T) { + + ctx := context.Background() + lim := &limiter.NoopLimiter[struct{}]{ + C: make(chan struct{}, 1), + } + reg := registry.NewRegistry() + ren := renderer.New(composition.NewCollection(), formatter.NewJsonFormatter()) + top := topic.New(reg) + cli := &client.MockClient{ + OnConnect: make(chan struct{}), + OnDisconnect: make(chan struct{}), + Observable: reg, + OnPublish: func(topic string, qos byte, messagesRetained bool, data []byte) error { + return nil + }, + } + go close(cli.OnConnect) + + env := environment.New() + + publisher := New(ctx, ren, cli, top, env, reg, tracer.NewNoopTracer(), lim) + + go close(cli.OnDisconnect) + publisher.Start() +} + +func TestPublisherDisconnectViaCancellation(t *testing.T) { + + ctx := context.Background() + lim := &limiter.NoopLimiter[struct{}]{ + C: make(chan struct{}, 1), + } + reg := registry.NewRegistry() + ren := renderer.New(composition.NewCollection(), formatter.NewJsonFormatter()) + top := topic.New(reg) + cli := &client.MockClient{ + OnConnect: make(chan struct{}), + OnDisconnect: make(chan struct{}), + Observable: reg, + OnPublish: func(topic string, qos byte, messagesRetained bool, data []byte) error { + return nil + }, + } + go close(cli.OnConnect) + + env := environment.New() + + publisher := New(ctx, ren, cli, top, env, reg, tracer.NewNoopTracer(), lim) + + go publisher.Cancel() + publisher.Start() +} \ No newline at end of file diff --git a/samples/krill/components/publisher/service.go b/samples/krill/components/publisher/service.go new file mode 100644 index 0000000..bfbd60f --- /dev/null +++ b/samples/krill/components/publisher/service.go @@ -0,0 +1,140 @@ +package publisher + +import ( + "context" + + "github.com/iot-for-all/device-simulation/components/client" + "github.com/iot-for-all/device-simulation/components/limiter" + "github.com/iot-for-all/device-simulation/components/observer" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/renderer" + "github.com/iot-for-all/device-simulation/components/topic" + "github.com/iot-for-all/device-simulation/components/tracer" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/environment" + "github.com/iot-for-all/device-simulation/lib/logger" +) + +type Store component.Store[*Publisher, component.ID] + +type Component struct { + RegistryID component.ID + ClientID component.ID + TopicID component.ID + RendererID component.ID + LimiterID component.ID + TracerID component.ID + QoSLevel int + RendersPerPublish int + MessagesRetained bool +} + +type Service struct { + Store + registryStore registry.Store + clientStore client.Store + topicStore topic.Store + rendererStore renderer.Store + limiterStore limiter.Store + tracerStore tracer.Store + ctx context.Context + Logger logger.Logger +} + +func NewStore() Store { + return component.New[*Publisher, component.ID]() +} + +func NewService( + ctx context.Context, + store Store, + registryStore registry.Store, + clientStore client.Store, + topicStore topic.Store, + rendererStore renderer.Store, + limiterStore limiter.Store, + tracerStore tracer.Store, + options ...func(*Service), +) *Service { + service := &Service{ + Store: store, + registryStore: registryStore, + clientStore: clientStore, + topicStore: topicStore, + rendererStore: rendererStore, + limiterStore: limiterStore, + tracerStore: tracerStore, + ctx: ctx, + Logger: &logger.NoopLogger{}, + } + + for _, option := range options { + option(service) + } + + return service +} + +func (service *Service) Create(id component.ID, c *Component) error { + var reg registry.Observable + reg, err := service.registryStore.Get(c.RegistryID) + if err != nil { + _, ok := err.(*component.NotFoundError) + if !ok { + return err + } + reg = &observer.NoopObservable{} + } + + var tra tracer.Tracer + tra, err = service.tracerStore.Get(c.TracerID) + if err != nil { + _, ok := err.(*component.NotFoundError) + if !ok { + return err + } + tra = tracer.NewNoopTracer() + } + + cli, err := service.clientStore.Get(c.ClientID) + if err != nil { + return err + } + + top, err := service.topicStore.Get(c.TopicID) + if err != nil { + return err + } + + lim, err := service.limiterStore.Get(c.LimiterID) + if err != nil { + return err + } + + ren, err := service.rendererStore.Get(c.RendererID) + if err != nil { + return err + } + + pub := New( + service.ctx, + ren, + cli, + top, + environment.New(), + reg, + tra, + lim, + func(p *Publisher) { + p.QoS = c.QoSLevel + p.RendersPerPublish = c.RendersPerPublish + p.MessagesRetained = c.MessagesRetained + p.Logger = service.Logger + p.Name = cli.GetName() + p.Site = cli.Render() + }, + ) + go pub.Start() + + return service.Store.Create(pub, id) +} diff --git a/samples/krill/components/publisher/service_test.go b/samples/krill/components/publisher/service_test.go new file mode 100644 index 0000000..e6bd925 --- /dev/null +++ b/samples/krill/components/publisher/service_test.go @@ -0,0 +1,228 @@ +package publisher + +import ( + "context" + "testing" + + "github.com/iot-for-all/device-simulation/components/client" + "github.com/iot-for-all/device-simulation/components/limiter" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/renderer" + "github.com/iot-for-all/device-simulation/components/topic" + "github.com/iot-for-all/device-simulation/components/tracer" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/logger" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockRegistryID = "MockRegistryID" + MockClientID = "MockClientID" + MockTopicID = "MockTopicID" + MockRendererID = "MockRendererID" + MockLimiterID = "MockLimiterID" + MockTracerID = "MockTracerID" + MockName = "MockName" + MockSite = "MockSite" + MockQoSLevel = 1 + MockRendersPerPublish = 1 + MockMessagesRetained = true +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[*Publisher, component.ID]) + require.True(t, ok) +} + +func TestPublisherService(t *testing.T) { + service := NewService(context.Background(), &component.MockStore[*Publisher, component.ID]{ + OnCreate: func(entity *Publisher, identifier component.ID) error { + require.Equal(t, MockName, entity.Name) + require.Equal(t, MockSite, entity.Site) + require.Equal(t, MockQoSLevel, entity.QoS) + require.Equal(t, MockRendersPerPublish, entity.RendersPerPublish) + require.Equal(t, MockMessagesRetained, entity.MessagesRetained) + require.Equal(t, MockID, string(identifier)) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return nil, nil + }, + }, &component.MockStore[client.PublisherSubscriber, component.ID]{ + OnGet: func(identifier component.ID) (client.PublisherSubscriber, error) { + require.Equal(t, MockClientID, string(identifier)) + return &client.MockClient{ + OnGetName: func() string { + return MockName + }, OnRender: func() string { + return MockSite + }, + }, nil + }, + }, &component.MockStore[topic.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (topic.Renderer, error) { + require.Equal(t, MockTopicID, string(identifier)) + return nil, nil + }, + }, &component.MockStore[renderer.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (renderer.Renderer, error) { + require.Equal(t, MockRendererID, string(identifier)) + return nil, nil + }, + }, &component.MockStore[limiter.Limiter[struct{}], component.ID]{ + OnGet: func(identifier component.ID) (limiter.Limiter[struct{}], error) { + require.Equal(t, MockLimiterID, string(identifier)) + return nil, nil + }, + }, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + require.Equal(t, MockTracerID, string(identifier)) + return nil, nil + }, + }, func(s *Service) { + s.Logger = &logger.NoopLogger{} + }) + + err := service.Create(MockID, &Component{ + RegistryID: MockRegistryID, + ClientID: MockClientID, + TopicID: MockTopicID, + RendererID: MockRendererID, + LimiterID: MockLimiterID, + TracerID: MockTracerID, + QoSLevel: MockQoSLevel, + RendersPerPublish: MockRendersPerPublish, + MessagesRetained: MockMessagesRetained, + }) + require.NoError(t, err) +} + +func TestPublisherServiceClientStoreError(t *testing.T) { + service := NewService(context.Background(), nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.NotFoundError{} + }, + }, &component.MockStore[client.PublisherSubscriber, component.ID]{ + OnGet: func(identifier component.ID) (client.PublisherSubscriber, error) { + return nil, &component.MockError{} + }, + }, nil, nil, nil, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + return nil, &component.NotFoundError{} + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} + +func TestPublisherServiceTopicStoreError(t *testing.T) { + service := NewService(context.Background(), nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }, &component.MockStore[client.PublisherSubscriber, component.ID]{ + OnGet: func(identifier component.ID) (client.PublisherSubscriber, error) { + return nil, nil + }, + }, &component.MockStore[topic.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (topic.Renderer, error) { + return nil, &component.MockError{} + }, + }, nil, nil, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} + +func TestPublisherServiceLimiterStoreError(t *testing.T) { + service := NewService(context.Background(), nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }, &component.MockStore[client.PublisherSubscriber, component.ID]{ + OnGet: func(identifier component.ID) (client.PublisherSubscriber, error) { + return nil, nil + }, + }, &component.MockStore[topic.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (topic.Renderer, error) { + return nil, nil + }, + }, nil, &component.MockStore[limiter.Limiter[struct{}], component.ID]{ + OnGet: func(identifier component.ID) (limiter.Limiter[struct{}], error) { + return nil, &component.MockError{} + }, + }, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} + +func TestPublisherServiceRendererStoreError(t *testing.T) { + service := NewService(context.Background(), nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }, &component.MockStore[client.PublisherSubscriber, component.ID]{ + OnGet: func(identifier component.ID) (client.PublisherSubscriber, error) { + return nil, nil + }, + }, &component.MockStore[topic.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (topic.Renderer, error) { + return nil, nil + }, + }, &component.MockStore[renderer.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (renderer.Renderer, error) { + return nil, &component.MockError{} + }, + }, &component.MockStore[limiter.Limiter[struct{}], component.ID]{ + OnGet: func(identifier component.ID) (limiter.Limiter[struct{}], error) { + return nil, nil + }, + }, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} + +func TestPublisherServiceTracerStoreError(t *testing.T) { + service := NewService(context.Background(), nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }, nil, nil, nil, nil, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} + +func TestPublisherServiceRegistryStoreError(t *testing.T) { + service := NewService(context.Background(), nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.MockError{} + }, + }, nil, nil, nil, nil, nil) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} diff --git a/samples/krill/components/registry/registry.go b/samples/krill/components/registry/registry.go new file mode 100644 index 0000000..97eb170 --- /dev/null +++ b/samples/krill/components/registry/registry.go @@ -0,0 +1,107 @@ +// Package registry contains the implementation of the monitor and observer components as well as other functionality related to configurable metrics and observability. +package registry + +import "sync" + +// Observable is an interface whose implementation should be able to observe a float64 value. +type Observable interface { + Observe(value float64) +} + +// Observable is an interface whose implementation should be able to observe a float64 value. +type CancellableObservable interface { + Observable + Cancel() +} + +// Registry is an interface whose implementation should be able to register and deregister observables. +type Registry interface { + Register(Observable) int + Deregister(int) +} + +type ObservableRegistry interface { + Observable + Registry +} + +// ObserverRegistry is an implementation of both Registry and Observable. +type ObserverRegistry struct { + observables map[int]Observable + mu sync.RWMutex + next int +} + +// NewRegistry will create an ObserverRegistry. +func NewRegistry() *ObserverRegistry { + return &ObserverRegistry{ + observables: make(map[int]Observable), + } +} + +// Register will add an observable to the registry's map, returning an identifier for that observable. +func (registry *ObserverRegistry) Register(observable Observable) int { + registry.mu.Lock() + defer registry.mu.Unlock() + next := registry.next + registry.observables[next] = observable + registry.next++ + return next +} + +// Deregister will remove an observable from the registry's map, given an identifier for that observable. +func (registry *ObserverRegistry) Deregister(identifier int) { + registry.mu.Lock() + defer registry.mu.Unlock() + delete(registry.observables, identifier) +} + +// Observe will call the observe function for all currently registered observables. +func (registry *ObserverRegistry) Observe(val float64) { + registry.mu.RLock() + defer registry.mu.RUnlock() + for _, observer := range registry.observables { + observer.Observe(val) + } +} + +type NoopRegistry struct{} + +func (reg *NoopRegistry) Register(Observable) int { + return 0 +} + +func (reg *NoopRegistry) Deregister(int) {} + +func (reg *NoopRegistry) Observe(value float64) {} + +type MockRegistry struct { + OnRegister func(Observable) int + OnDeregister func(int) + OnObserve func(float64) +} + +func (reg *MockRegistry) Register(o Observable) int { + return reg.OnRegister(o) +} + +func (reg *MockRegistry) Deregister(i int) { + reg.OnDeregister(i) +} + +func (reg *MockRegistry) Observe(f float64) { + reg.OnObserve(f) +} + +type MockObservable struct { + OnObserve func(val float64) + OnCancel func() +} + +func (obs *MockObservable) Observe(val float64) { + obs.OnObserve(val) +} + +func (obs *MockObservable) Cancel() { + obs.OnCancel() +} diff --git a/samples/krill/components/registry/registry_test.go b/samples/krill/components/registry/registry_test.go new file mode 100644 index 0000000..0926bb5 --- /dev/null +++ b/samples/krill/components/registry/registry_test.go @@ -0,0 +1,58 @@ +package registry + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestBasicRegistry(t *testing.T) { + + expected := 101.0 + reg := NewRegistry() + observed := make(chan struct{}) + obs := &MockObservable{ + OnObserve: func(val float64) { + require.Equal(t, expected, val) + close(observed) + }, + } + + reg.Register(obs) + + go reg.Observe(expected) + <-observed + + require.Equal(t, 1, len(reg.observables)) +} + +func TestRegistryWithMultipleObservables(t *testing.T) { + expected := 101.0 + reg := NewRegistry() + observed := make(chan struct{}) + obsOne := &MockObservable{ + OnObserve: func(val float64) { + require.Equal(t, expected, val) + observed <- struct{}{} + }, + } + obsTwo := &MockObservable{ + OnObserve: func(val float64) { + require.Equal(t, expected, val) + observed <- struct{}{} + }, + } + reg.Register(obsOne) + reg.Register(obsTwo) + + go reg.Observe(expected) + <-observed + <-observed + close(observed) + + require.Equal(t, 2, len(reg.observables)) +} \ No newline at end of file diff --git a/samples/krill/components/registry/service.go b/samples/krill/components/registry/service.go new file mode 100644 index 0000000..f81dca1 --- /dev/null +++ b/samples/krill/components/registry/service.go @@ -0,0 +1,28 @@ +package registry + +import ( + "github.com/iot-for-all/device-simulation/lib/component" +) + +type Store component.Store[ObservableRegistry, component.ID] + +type Component struct{} + +type Service struct { + Store +} + +func NewStore() Store { + return component.New[ObservableRegistry, component.ID]() +} + +func NewService(store Store) *Service { + return &Service{ + Store: store, + } +} + +func (service *Service) Create(id component.ID, c *Component) error { + return service.Store.Create( + NewRegistry(), id) +} diff --git a/samples/krill/components/registry/service_test.go b/samples/krill/components/registry/service_test.go new file mode 100644 index 0000000..260ef71 --- /dev/null +++ b/samples/krill/components/registry/service_test.go @@ -0,0 +1,27 @@ +package registry + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/stretchr/testify/require" +) + +const MockID = "MockID" + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[ObservableRegistry, component.ID]) + require.True(t, ok) +} + +func TestService(t *testing.T) { + service := NewService(&component.MockStore[ObservableRegistry, component.ID]{ + OnCreate: func(entity ObservableRegistry, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + return nil + }, + }) + + require.NoError(t, service.Create(MockID, nil)) +} diff --git a/samples/krill/components/renderer/renderer.go b/samples/krill/components/renderer/renderer.go new file mode 100644 index 0000000..a1f7cfd --- /dev/null +++ b/samples/krill/components/renderer/renderer.go @@ -0,0 +1,40 @@ +package renderer + +import ( + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/lib/composition" + "github.com/iot-for-all/device-simulation/lib/environment" +) + +type Renderer interface { + Render(environment.Environment, int, int) ([]byte, error) +} + +type NodeRenderer struct { + node composition.Renderer + formatter formatter.Formatter +} + +func New(nd composition.Renderer, frmt formatter.Formatter) *NodeRenderer { + return &NodeRenderer{ + node: nd, + formatter: frmt, + } +} + +func (renderer *NodeRenderer) Render( + env environment.Environment, + start, rows int, +) ([]byte, error) { + + res := make([]any, rows) + + for idx := 0; idx < rows; idx++ { + env.Set("x", start+idx) + rendered := renderer.node.Render(env.Env()) + res[idx] = rendered + env.Set("p", rendered) + } + + return renderer.formatter.Format(res) +} \ No newline at end of file diff --git a/samples/krill/components/renderer/renderer_test.go b/samples/krill/components/renderer/renderer_test.go new file mode 100644 index 0000000..f5d46a5 --- /dev/null +++ b/samples/krill/components/renderer/renderer_test.go @@ -0,0 +1,47 @@ +package renderer + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/lib/composition" + "github.com/iot-for-all/device-simulation/lib/environment" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestRenderer(t *testing.T) { + expected := 1 + expectedArr := []any{expected, expected} + renderer := New(&composition.MockRenderer{ + OnRender: func(m map[string]any) any { + require.Equal(t, expected, m[""]) + return expected + }, + }, &formatter.MockFormatter{ + OnFormat: func(a any) ([]byte, error) { + require.Equal(t, expectedArr, a) + return nil, nil + }, + }) + + count := 0 + + _, err := renderer.Render(&environment.MockEnvironment{ + OnSet: func(s string, a any) { + if s == "x" { + require.Equal(t, count, a) + count++ + } else { + require.Equal(t, expected, a) + } + }, OnEnv: func() map[string]any { + return map[string]any{"": expected} + }, + }, 0, 2) + + require.NoError(t, err) +} \ No newline at end of file diff --git a/samples/krill/components/renderer/service.go b/samples/krill/components/renderer/service.go new file mode 100644 index 0000000..a613f65 --- /dev/null +++ b/samples/krill/components/renderer/service.go @@ -0,0 +1,47 @@ +package renderer + +import ( + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/components/node" + "github.com/iot-for-all/device-simulation/lib/component" +) + +type Store component.Store[Renderer, component.ID] + +type Component struct { + FormatterID component.ID + NodeID component.ID +} + +type Service struct { + Store + formatterStore formatter.Store + nodeStore node.Store +} + +func NewStore() Store { + return component.New[Renderer, component.ID]() +} + +func NewService(store Store, formatterStore formatter.Store, nodeStore node.Store) *Service { + return &Service{ + Store: store, + formatterStore: formatterStore, + nodeStore: nodeStore, + } +} + +func (service *Service) Create(id component.ID, c *Component) error { + + fmtr, err := service.formatterStore.Get(c.FormatterID) + if err != nil { + return err + } + + nd, err := service.nodeStore.Get(c.NodeID) + if err != nil { + return err + } + + return service.Store.Create(New(nd, fmtr), id) +} diff --git a/samples/krill/components/renderer/service_test.go b/samples/krill/components/renderer/service_test.go new file mode 100644 index 0000000..3febf36 --- /dev/null +++ b/samples/krill/components/renderer/service_test.go @@ -0,0 +1,79 @@ +package renderer + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/composition" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockFormatterID = "MockFormatterID" + MockNodeID = "MockNodeID" +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[Renderer, component.ID]) + require.True(t, ok) +} + +func TestRendererService(t *testing.T) { + service := NewService(&component.MockStore[Renderer, component.ID]{ + OnCreate: func(entity Renderer, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + return nil + }, + }, &component.MockStore[formatter.Formatter, component.ID]{ + OnGet: func(identifier component.ID) (formatter.Formatter, error) { + require.Equal(t, MockFormatterID, string(identifier)) + return nil, nil + }, + }, &component.MockStore[composition.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (composition.Renderer, error) { + require.Equal(t, MockNodeID, string(identifier)) + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{ + FormatterID: MockFormatterID, + NodeID: MockNodeID, + }) + require.NoError(t, err) +} + +func TestRendererServiceNodeStoreError(t *testing.T) { + service := NewService(nil, &component.MockStore[formatter.Formatter, component.ID]{ + OnGet: func(identifier component.ID) (formatter.Formatter, error) { + return nil, nil + }, + }, &component.MockStore[composition.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (composition.Renderer, error) { + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{ + FormatterID: MockFormatterID, + NodeID: MockNodeID, + }) + require.Equal(t, &component.MockError{}, err) +} + +func TestRendererServiceFormatterStoreError(t *testing.T) { + service := NewService(nil, &component.MockStore[formatter.Formatter, component.ID]{ + OnGet: func(identifier component.ID) (formatter.Formatter, error) { + return nil, &component.MockError{} + }, + }, nil) + + err := service.Create(MockID, &Component{ + FormatterID: MockFormatterID, + NodeID: MockNodeID, + }) + require.Equal(t, &component.MockError{}, err) +} diff --git a/samples/krill/components/site/service.go b/samples/krill/components/site/service.go new file mode 100644 index 0000000..274d95a --- /dev/null +++ b/samples/krill/components/site/service.go @@ -0,0 +1,46 @@ +package site + +import ( + "github.com/iot-for-all/device-simulation/components/observer" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" +) + +type Store component.Store[Site, component.ID] + +type Component struct { + Name string + RegistryID component.ID +} + +type Service struct { + Store + registryStore registry.Store +} + +func NewStore() Store { + return component.New[Site, component.ID]() +} + +func NewService(store Store, registryStore registry.Store) *Service { + return &Service{ + Store: store, + registryStore: registryStore, + } +} + +func (service *Service) Create(id component.ID, c *Component) error { + var reg registry.Observable + reg, err := service.registryStore.Get(c.RegistryID) + if err != nil { + _, ok := err.(*component.NotFoundError) + if !ok { + return err + } + reg = &observer.NoopObservable{} + } + + return service.Store.Create(New(reg, func(ss *StaticSite) { + ss.Name = c.Name + }), id) +} diff --git a/samples/krill/components/site/service_test.go b/samples/krill/components/site/service_test.go new file mode 100644 index 0000000..6bcd42a --- /dev/null +++ b/samples/krill/components/site/service_test.go @@ -0,0 +1,74 @@ +package site + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockRegistryID = "MockRegistryID" + MockSiteName = "MockSiteName" +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[Site, component.ID]) + require.True(t, ok) +} + +func TestSiteService(t *testing.T) { + service := NewService(&component.MockStore[Site, component.ID]{ + OnCreate: func(entity Site, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + require.Equal(t, MockSiteName, entity.Render()) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{ + Name: MockSiteName, + RegistryID: MockRegistryID, + }) + require.NoError(t, err) +} + +func TestSiteServiceRegistryError(t *testing.T) { + service := NewService(nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} + +func TestSiteServiceNoopRegistry(t *testing.T) { + service := NewService(&component.MockStore[Site, component.ID]{ + OnCreate: func(entity Site, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + require.Equal(t, MockSiteName, entity.Render()) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return nil, &component.NotFoundError{} + }, + }) + + err := service.Create(MockID, &Component{ + Name: MockSiteName, + RegistryID: MockRegistryID, + }) + require.NoError(t, err) +} diff --git a/samples/krill/components/site/site.go b/samples/krill/components/site/site.go new file mode 100644 index 0000000..b92f704 --- /dev/null +++ b/samples/krill/components/site/site.go @@ -0,0 +1,43 @@ +// Package site provides the implementation of the site component of the simulation framework. +package site + +import "github.com/iot-for-all/device-simulation/components/registry" + +type Site interface { + Render() string + registry.Observable +} + +// Site is a representation of a place where many devices/clients may be present. +// It implements the observable interface, allowing for monitoring of all devices within a site. +type StaticSite struct { + registry.Observable + Name string +} + +// New creates a new site, given an observable monitor. +// Optional parameters can be set through the option function. +func New(mon registry.Observable, options ...func(*StaticSite)) *StaticSite { + site := &StaticSite{ + Observable: mon, + } + + for _, option := range options { + option(site) + } + + return site +} + +func (site *StaticSite) Render() string { + return site.Name +} + +type MockSite struct { + OnRender func() string + registry.Observable +} + +func (site *MockSite) Render() string { + return site.OnRender() +} diff --git a/samples/krill/components/site/site_test.go b/samples/krill/components/site/site_test.go new file mode 100644 index 0000000..38b8097 --- /dev/null +++ b/samples/krill/components/site/site_test.go @@ -0,0 +1,17 @@ +package site + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestSite(t *testing.T) { + site := New(nil) + + require.Equal(t, "", site.Name) +} diff --git a/samples/krill/components/subscriber/service.go b/samples/krill/components/subscriber/service.go new file mode 100644 index 0000000..031686e --- /dev/null +++ b/samples/krill/components/subscriber/service.go @@ -0,0 +1,116 @@ +package subscriber + +import ( + "github.com/iot-for-all/device-simulation/components/client" + "github.com/iot-for-all/device-simulation/components/observer" + "github.com/iot-for-all/device-simulation/components/outlet" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/topic" + "github.com/iot-for-all/device-simulation/components/tracer" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/logger" +) + +type Store component.Store[*Subscriber, component.ID] + +type Component struct { + ClientID component.ID + TopicID component.ID + OutletID component.ID + RegistryID component.ID + TracerID component.ID + QoSLevel int +} + +type Service struct { + Store + clientStore client.Store + topicStore topic.Store + outletStore outlet.Store + registryStore registry.Store + tracerStore tracer.Store + Logger logger.Logger +} + +func NewStore() Store { + return component.New[*Subscriber, component.ID]() +} + +func NewService( + store Store, + clientStore client.Store, + topicStore topic.Store, + outletStore outlet.Store, + registryStore registry.Store, + tracerStore tracer.Store, + options ...func(*Service), +) *Service { + service := &Service{ + Store: store, + clientStore: clientStore, + topicStore: topicStore, + outletStore: outletStore, + registryStore: registryStore, + tracerStore: tracerStore, + Logger: &logger.NoopLogger{}, + } + + for _, option := range options { + option(service) + } + + return service +} + +func (service *Service) Create(id component.ID, c *Component) error { + var reg registry.Observable + reg, err := service.registryStore.Get(c.RegistryID) + if err != nil { + _, ok := err.(*component.NotFoundError) + if !ok { + return err + } + reg = &observer.NoopObservable{} + } + + var tra tracer.Tracer + tra, err = service.tracerStore.Get(c.TracerID) + if err != nil { + _, ok := err.(*component.NotFoundError) + if !ok { + return err + } + tra = tracer.NewNoopTracer() + } + + var out outlet.Outlet + out, err = service.outletStore.Get(c.OutletID) + if err != nil { + _, ok := err.(*component.NotFoundError) + if !ok { + return err + } + out = &outlet.NoopOutlet{} + } + + cli, err := service.clientStore.Get(c.ClientID) + if err != nil { + return err + } + + top, err := service.topicStore.Get(c.TopicID) + if err != nil { + return err + } + + sub := New(cli, top, out, reg, tra, func(s *Subscriber) { + s.QoS = c.QoSLevel + s.Logger = service.Logger.With("topic", top.Render()).With("client", cli.GetName()).With("site", cli.Render()) + }) + err = sub.Start() + if err != nil { + return err + } + + return service.Store.Create(sub, id) +} diff --git a/samples/krill/components/subscriber/service_test.go b/samples/krill/components/subscriber/service_test.go new file mode 100644 index 0000000..d73306b --- /dev/null +++ b/samples/krill/components/subscriber/service_test.go @@ -0,0 +1,267 @@ +package subscriber + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/client" + "github.com/iot-for-all/device-simulation/components/outlet" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/topic" + "github.com/iot-for-all/device-simulation/components/tracer" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/logger" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockClientID = "MockClientID" + MockTopicID = "MockTopicID" + MockOutletID = "MockOutletID" + MockRegistryID = "MockRegistryID" + MockTracerID = "MockTracerID" + MockName = "MockName" + MockTopic = "MockTopic" + MockSiteName = "MockSiteName" + MockQoSLevel = 1 +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[*Subscriber, component.ID]) + require.True(t, ok) +} + +func TestService(t *testing.T) { + service := NewService(&component.MockStore[*Subscriber, component.ID]{ + OnCreate: func(entity *Subscriber, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + return nil + }, + }, &component.MockStore[client.PublisherSubscriber, component.ID]{ + OnGet: func(identifier component.ID) (client.PublisherSubscriber, error) { + require.Equal(t, MockClientID, string(identifier)) + return &MockClient{ + PublisherSubscriber: &client.MockClient{ + OnGetName: func() string { + return MockName + }, OnRender: func() string { + return MockSiteName + }, OnSubscribe: func(topic string, qos byte, onReceived func([]byte)) error { + require.Equal(t, MockTopic, topic) + require.Equal(t, byte(MockQoSLevel), qos) + return nil + }, + }, OnConnected: func() chan struct{} { + c := make(chan struct{}) + close(c) + return c + }, OnDisconnected: func() chan struct{} { + c := make(chan struct{}) + close(c) + return c + }, + }, nil + }, + }, &component.MockStore[topic.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (topic.Renderer, error) { + require.Equal(t, MockTopicID, string(identifier)) + return &topic.Topic{ + Topic: MockTopic, + }, nil + }, + }, &component.MockStore[outlet.Outlet, component.ID]{ + OnGet: func(identifier component.ID) (outlet.Outlet, error) { + require.Equal(t, MockOutletID, string(identifier)) + return nil, nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return nil, nil + }, + }, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + require.Equal(t, MockTracerID, string(identifier)) + return nil, nil + }, + }, func(s *Service) { + s.Logger = &logger.NoopLogger{} + }) + + err := service.Create(MockID, &Component{ + ClientID: MockClientID, + TopicID: MockTopicID, + OutletID: MockOutletID, + RegistryID: MockRegistryID, + TracerID: MockTracerID, + QoSLevel: MockQoSLevel, + }) + + require.NoError(t, err) +} + +func TestServiceSubscriptionError(t *testing.T) { + service := NewService(&component.MockStore[*Subscriber, component.ID]{ + OnCreate: func(entity *Subscriber, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + return nil + }, + }, &component.MockStore[client.PublisherSubscriber, component.ID]{ + OnGet: func(identifier component.ID) (client.PublisherSubscriber, error) { + require.Equal(t, MockClientID, string(identifier)) + return &MockClient{ + PublisherSubscriber: &client.MockClient{ + OnGetName: func() string { + return MockName + }, OnRender: func() string { + return MockSiteName + }, OnSubscribe: func(topic string, qos byte, onReceived func([]byte)) error { + return &component.MockError{} + }, + }, OnConnected: func() chan struct{} { + c := make(chan struct{}) + close(c) + return c + }, OnDisconnected: func() chan struct{} { + c := make(chan struct{}) + close(c) + return c + }, + }, nil + }, + }, &component.MockStore[topic.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (topic.Renderer, error) { + require.Equal(t, MockTopicID, string(identifier)) + return &topic.Topic{ + Topic: MockTopic, + }, nil + }, + }, &component.MockStore[outlet.Outlet, component.ID]{ + OnGet: func(identifier component.ID) (outlet.Outlet, error) { + require.Equal(t, MockOutletID, string(identifier)) + return nil, nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return nil, nil + }, + }, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + require.Equal(t, MockTracerID, string(identifier)) + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{ + ClientID: MockClientID, + TopicID: MockTopicID, + OutletID: MockOutletID, + RegistryID: MockRegistryID, + TracerID: MockTracerID, + QoSLevel: MockQoSLevel, + }) + + require.Equal(t, &component.MockError{}, err) +} + +func TestServiceTopicStoreError(t *testing.T) { + service := NewService(nil, &component.MockStore[client.PublisherSubscriber, component.ID]{ + OnGet: func(identifier component.ID) (client.PublisherSubscriber, error) { + return nil, nil + }, + }, &component.MockStore[topic.Renderer, component.ID]{ + OnGet: func(identifier component.ID) (topic.Renderer, error) { + return nil, &component.MockError{} + }, + }, &component.MockStore[outlet.Outlet, component.ID]{ + OnGet: func(identifier component.ID) (outlet.Outlet, error) { + return nil, nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{}) + + require.Equal(t, &component.MockError{}, err) +} + +func TestServiceClientStoreError(t *testing.T) { + service := NewService(nil, &component.MockStore[client.PublisherSubscriber, component.ID]{ + OnGet: func(identifier component.ID) (client.PublisherSubscriber, error) { + return nil, &component.MockError{} + }, + }, nil, &component.MockStore[outlet.Outlet, component.ID]{ + OnGet: func(identifier component.ID) (outlet.Outlet, error) { + return nil, &component.NotFoundError{} + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.NotFoundError{} + }, + }, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + return nil, &component.NotFoundError{} + }, + }) + + err := service.Create(MockID, &Component{}) + + require.Equal(t, &component.MockError{}, err) +} + +func TestServiceOutletStoreError(t *testing.T) { + service := NewService(nil, nil, nil, &component.MockStore[outlet.Outlet, component.ID]{ + OnGet: func(identifier component.ID) (outlet.Outlet, error) { + return nil, &component.MockError{} + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{}) + + require.Equal(t, &component.MockError{}, err) +} + +func TestServiceTracerStoreError(t *testing.T) { + service := NewService(nil, nil, nil, nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, nil + }, + }, &component.MockStore[tracer.Tracer, component.ID]{ + OnGet: func(identifier component.ID) (tracer.Tracer, error) { + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{}) + + require.Equal(t, &component.MockError{}, err) +} + +func TestServiceRegistryStoreError(t *testing.T) { + service := NewService(nil, nil, nil, nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.MockError{} + }, + }, nil) + + err := service.Create(MockID, &Component{}) + + require.Equal(t, &component.MockError{}, err) +} \ No newline at end of file diff --git a/samples/krill/components/subscriber/subscriber.go b/samples/krill/components/subscriber/subscriber.go new file mode 100644 index 0000000..0c14c3a --- /dev/null +++ b/samples/krill/components/subscriber/subscriber.go @@ -0,0 +1,115 @@ +// Package subscriber provides the implementation for the subscriber component of the simulation framework. +package subscriber + +import ( + "fmt" + + "github.com/iot-for-all/device-simulation/components/client" + "github.com/iot-for-all/device-simulation/components/outlet" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/topic" + "github.com/iot-for-all/device-simulation/components/tracer" + "github.com/iot-for-all/device-simulation/lib/logger" +) + +type ClientConnectionClosedError struct { + client string +} + +func (err *ClientConnectionClosedError) Error() string { + return fmt.Sprintf( + "the connection of client with id %s was already closed before unsubscribing", + err.client, + ) +} + +// Subscriber is a component which listens for incoming messages on an MQTT broker whose connection +// is provided by the underlying client component. +type Subscriber struct { + client client.PublisherSubscriber + topic topic.Renderer + tracer tracer.Tracer + outlet outlet.Outlet + monitor registry.Observable + Logger logger.Logger + onReceive func([]byte) + QoS int +} + +// New creates a Subscriber, given a client and topic component. +// Optional parameters can be set using the options function. +func New( + cli client.PublisherSubscriber, + top topic.Renderer, + out outlet.Outlet, + mon registry.Observable, + tra tracer.Tracer, + options ...func(*Subscriber), +) *Subscriber { + + subscriber := &Subscriber{ + client: cli, + topic: top, + outlet: out, + monitor: mon, + tracer: tra, + Logger: &logger.NoopLogger{}, + QoS: 0, + } + + for _, option := range options { + option(subscriber) + } + + errLvl := subscriber.Logger.Level(logger.Error) + traceLvl := subscriber.Logger.Level(logger.Trace) + + subscriber.onReceive = func(b []byte) { + traceLvl.Printf("received new message") + subscriber.monitor.Observe(0) + err := subscriber.outlet.Observe(b) + if err != nil { + errLvl.With("error", err.Error()). + Printf("error occurred when observing received message") + } + subscriber.tracer.Received() + } + + return subscriber +} + +// Start will wait until the underlying client is connected, and then subscribe to the originally provided topic once connected. +func (subscriber *Subscriber) Start() error { + <-subscriber.client.Connected() + return subscriber.client.Subscribe( + subscriber.topic.Render(), + byte(subscriber.QoS), + subscriber.onReceive, + ) +} + +// Cancel will return an error if the client is already disconnected, and will attempt to unsubscribe if not. +func (subscriber *Subscriber) Cancel() error { + select { + case <-subscriber.client.Disconnected(): + return &ClientConnectionClosedError{ + client: subscriber.client.GetName(), + } + default: + return subscriber.client.Unsubscribe(subscriber.topic.Render()) + } +} + +type MockClient struct { + client.PublisherSubscriber + OnConnected func() chan struct{} + OnDisconnected func() chan struct{} +} + +func (client *MockClient) Connected() chan struct{} { + return client.OnConnected() +} + +func (client *MockClient) Disconnected() chan struct{} { + return client.OnDisconnected() +} diff --git a/samples/krill/components/subscriber/subscriber_test.go b/samples/krill/components/subscriber/subscriber_test.go new file mode 100644 index 0000000..da0d3de --- /dev/null +++ b/samples/krill/components/subscriber/subscriber_test.go @@ -0,0 +1,152 @@ +package subscriber + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/client" + "github.com/iot-for-all/device-simulation/components/outlet" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/topic" + "github.com/iot-for-all/device-simulation/components/tracer" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestSubscribeAfterClientConnection(t *testing.T) { + onSub := make(chan struct{}) + cli := &client.MockClient{ + OnSubscribe: func(topic string, qos byte, onReceived func([]byte)) error { + close(onSub) + return nil + }, OnUnsubscribe: func(topic string) error { + return nil + }, OnDisconnect: make(chan struct{}), + OnConnect: make(chan struct{}), + } + sub := New(cli, topic.New(nil), &outlet.NoopOutlet{}, ®istry.NoopRegistry{}, tracer.NewNoopTracer()) + go func() { + err := sub.Start() + require.NoError(t, err) + }() + err := cli.Connect() + require.NoError(t, err) + <-onSub +} + +func TestReceivedChannelWithBlocking(t *testing.T) { + onSub := make(chan struct{}) + cli := &client.MockClient{ + OnSubscribe: func(topic string, qos byte, onReceived func([]byte)) error { + close(onSub) + onReceived(nil) + return nil + }, OnUnsubscribe: func(topic string) error { + return nil + }, OnDisconnect: make(chan struct{}), + OnConnect: make(chan struct{}), + } + sub := New( + cli, + topic.New(nil), + &outlet.NoopOutlet{}, + ®istry.NoopRegistry{}, + tracer.NewNoopTracer(), + func(s *Subscriber) {}, + ) + go func() { + err := sub.Start() + require.NoError(t, err) + }() + err := cli.Connect() + require.NoError(t, err) + <-onSub +} + +func TestReceivedChannelWithoutBlocking(t *testing.T) { + onSub := make(chan struct{}) + afterOnReceivedCalled := make(chan struct{}) + cli := &client.MockClient{ + OnSubscribe: func(topic string, qos byte, onReceived func([]byte)) error { + close(onSub) + onReceived(nil) + close(afterOnReceivedCalled) + return nil + }, OnUnsubscribe: func(topic string) error { + return nil + }, OnDisconnect: make(chan struct{}), + OnConnect: make(chan struct{}), + } + sub := New(cli, topic.New(nil), &outlet.NoopOutlet{}, ®istry.NoopRegistry{}, tracer.NewNoopTracer()) + go func() { + err := sub.Start() + require.NoError(t, err) + }() + err := cli.Connect() + require.NoError(t, err) + <-onSub + <-afterOnReceivedCalled +} + +func TestSubscriberCancelBeforeClientCancel(t *testing.T) { + onSub := make(chan struct{}) + onUnsub := make(chan struct{}) + + cli := &client.MockClient{ + OnSubscribe: func(topic string, qos byte, onReceived func([]byte)) error { + close(onSub) + return nil + }, OnUnsubscribe: func(topic string) error { + close(onUnsub) + return nil + }, OnDisconnect: make(chan struct{}), + OnConnect: make(chan struct{}), + } + sub := New(cli, topic.New(nil), &outlet.NoopOutlet{}, ®istry.NoopRegistry{}, tracer.NewNoopTracer()) + go func() { + require.NoError(t, sub.Start()) + }() + err := cli.Connect() + require.NoError(t, err) + <-onSub + err = sub.Cancel() + require.NoError(t, err) + <-onUnsub +} + +func TestSubscriberCancelAfterClientCancel(t *testing.T) { + onSub := make(chan struct{}) + onDisconnect := make(chan struct{}) + name := "name" + + cli := &client.MockClient{ + OnSubscribe: func(topic string, qos byte, onReceived func([]byte)) error { + close(onSub) + return nil + }, OnUnsubscribe: func(topic string) error { + return nil + }, OnDisconnect: onDisconnect, + OnConnect: make(chan struct{}), + OnGetName: func() string { + return name + }, + } + sub := New(cli, topic.New(nil), &outlet.NoopOutlet{}, ®istry.NoopRegistry{}, tracer.NewNoopTracer()) + go func() { + require.NoError(t, sub.Start()) + }() + err := cli.Connect() + require.NoError(t, err) + <-onSub + go func() { + err := cli.Disconnect() + require.NoError(t, err) + }() + <-onDisconnect + err = sub.Cancel() + require.Equal(t, ClientConnectionClosedError{ + client: name, + }, *err.(*ClientConnectionClosedError)) +} \ No newline at end of file diff --git a/samples/krill/components/topic/service.go b/samples/krill/components/topic/service.go new file mode 100644 index 0000000..185f667 --- /dev/null +++ b/samples/krill/components/topic/service.go @@ -0,0 +1,46 @@ +package topic + +import ( + "github.com/iot-for-all/device-simulation/components/observer" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" +) + +type Store component.Store[Renderer, component.ID] + +type Component struct { + RegistryID component.ID + Name string +} + +type Service struct { + Store + registryStore registry.Store +} + +func NewStore() Store { + return component.New[Renderer, component.ID]() +} + +func NewService(store Store, registryStore registry.Store) *Service { + return &Service{ + Store: store, + registryStore: registryStore, + } +} + +func (service *Service) Create(id component.ID, c *Component) error { + var reg registry.Observable + reg, err := service.registryStore.Get(c.RegistryID) + if err != nil { + _, ok := err.(*component.NotFoundError) + if !ok { + return err + } + reg = &observer.NoopObservable{} + } + + return service.Store.Create(New(reg, func(t *Topic) { + t.Topic = c.Name + }), id) +} diff --git a/samples/krill/components/topic/service_test.go b/samples/krill/components/topic/service_test.go new file mode 100644 index 0000000..95d4ebd --- /dev/null +++ b/samples/krill/components/topic/service_test.go @@ -0,0 +1,74 @@ +package topic + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockRegistryID = "MockRegistryID" + MockTopic = "MockTopic" +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[Renderer, component.ID]) + require.True(t, ok) +} + +func TestTopicService(t *testing.T) { + service := NewService(&component.MockStore[Renderer, component.ID]{ + OnCreate: func(entity Renderer, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + require.Equal(t, MockTopic, entity.Render()) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return nil, nil + }, + }) + + err := service.Create(MockID, &Component{ + Name: MockTopic, + RegistryID: MockRegistryID, + }) + require.NoError(t, err) +} + +func TestTopicServiceNoopRegistry(t *testing.T) { + service := NewService(&component.MockStore[Renderer, component.ID]{ + OnCreate: func(entity Renderer, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + require.Equal(t, MockTopic, entity.Render()) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return nil, &component.NotFoundError{} + }, + }) + + err := service.Create(MockID, &Component{ + Name: MockTopic, + RegistryID: MockRegistryID, + }) + require.NoError(t, err) +} + +func TestTopicServiceRegistryError(t *testing.T) { + service := NewService(nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.MockError{} + }, + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} diff --git a/samples/krill/components/topic/topic.go b/samples/krill/components/topic/topic.go new file mode 100644 index 0000000..d0594b6 --- /dev/null +++ b/samples/krill/components/topic/topic.go @@ -0,0 +1,38 @@ +// Package topic provides the implementation for the topic component of the simulation framework. +package topic + +import ( + "github.com/iot-for-all/device-simulation/components/registry" +) + +// Renderer is an interface whose implementation should be able to render a topic name when called and should also implement the registry observable interface. +type Renderer interface { + Render() string + registry.Observable +} + +// Topic is an implementation of the Renderer interface which has the ability to render a predefined topic name when its Render function is called. +type Topic struct { + Topic string + registry.Observable +} + +// New creates a topic given an observable. +// Optional parameters can be provided using the options function. +func New(mon registry.Observable, options ...func(*Topic)) *Topic { + topic := &Topic{ + Topic: "/", + Observable: mon, + } + + for _, option := range options { + option(topic) + } + + return topic +} + +// Render returns the predefined topic name. +func (topic *Topic) Render() string { + return topic.Topic +} diff --git a/samples/krill/components/topic/topic_test.go b/samples/krill/components/topic/topic_test.go new file mode 100644 index 0000000..bb70409 --- /dev/null +++ b/samples/krill/components/topic/topic_test.go @@ -0,0 +1,25 @@ +package topic + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestDefaultTopic(t *testing.T) { + topic := New(nil) + require.Equal(t, "/", topic.Render()) +} + +func TestTopic(t *testing.T) { + expectedTopic := "/example/topic" + topic := New(nil, func(t *Topic) { + t.Topic = expectedTopic + }) + + require.Equal(t, expectedTopic, topic.Render()) +} diff --git a/samples/krill/components/tracer/service.go b/samples/krill/components/tracer/service.go new file mode 100644 index 0000000..c878640 --- /dev/null +++ b/samples/krill/components/tracer/service.go @@ -0,0 +1,54 @@ +package tracer + +import ( + "github.com/iot-for-all/device-simulation/components/observer" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/logger" +) + +type Store component.Store[Tracer, component.ID] + +type Component struct { + RegistryID component.ID +} + +type Service struct { + Store + registryStore registry.Store + Logger logger.Logger +} + +func NewStore() Store { + return component.New[Tracer, component.ID]() +} + +func NewService(store Store, registryStore registry.Store, options ...func(*Service)) *Service { + service := &Service{ + Store: store, + registryStore: registryStore, + Logger: &logger.NoopLogger{}, + } + + for _, option := range options { + option(service) + } + + return service +} + +func (service *Service) Create(id component.ID, c *Component) error { + var reg registry.Observable + reg, err := service.registryStore.Get(c.RegistryID) + if err != nil { + _, ok := err.(*component.NotFoundError) + if !ok { + return err + } + reg = &observer.NoopObservable{} + } + + return service.Store.Create(New(reg, func(bt *BlockingTracer) { + bt.Logger = service.Logger + }), id) +} diff --git a/samples/krill/components/tracer/service_test.go b/samples/krill/components/tracer/service_test.go new file mode 100644 index 0000000..d9871b6 --- /dev/null +++ b/samples/krill/components/tracer/service_test.go @@ -0,0 +1,76 @@ +package tracer + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/logger" + "github.com/stretchr/testify/require" +) + +const ( + MockID = "MockID" + MockRegistryID = "MockRegistryID" +) + +func TestStore(t *testing.T) { + store := NewStore() + _, ok := store.(*component.Memstore[Tracer, component.ID]) + require.True(t, ok) +} + +func TestTracerService(t *testing.T) { + service := NewService(&component.MockStore[Tracer, component.ID]{ + OnCreate: func(entity Tracer, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return nil, nil + }, + }, func(s *Service) { + s.Logger = &logger.NoopLogger{} + }) + + err := service.Create(MockID, &Component{ + RegistryID: MockRegistryID, + }) + require.NoError(t, err) +} + +func TestTracerServiceNoopRegistry(t *testing.T) { + service := NewService(&component.MockStore[Tracer, component.ID]{ + OnCreate: func(entity Tracer, identifier component.ID) error { + require.Equal(t, MockID, string(identifier)) + return nil + }, + }, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + require.Equal(t, MockRegistryID, string(identifier)) + return nil, &component.NotFoundError{} + }, + }, func(s *Service) { + s.Logger = &logger.NoopLogger{} + }) + + err := service.Create(MockID, &Component{ + RegistryID: MockRegistryID, + }) + require.NoError(t, err) +} + +func TestTracerServiceRegistryError(t *testing.T) { + service := NewService(nil, &component.MockStore[registry.ObservableRegistry, component.ID]{ + OnGet: func(identifier component.ID) (registry.ObservableRegistry, error) { + return nil, &component.MockError{} + }, + }, func(s *Service) { + s.Logger = &logger.NoopLogger{} + }) + + err := service.Create(MockID, &Component{}) + require.Equal(t, &component.MockError{}, err) +} diff --git a/samples/krill/components/tracer/tracer.go b/samples/krill/components/tracer/tracer.go new file mode 100644 index 0000000..5ad3054 --- /dev/null +++ b/samples/krill/components/tracer/tracer.go @@ -0,0 +1,71 @@ +package tracer + +import ( + "fmt" + "time" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/logger" +) + +type Tracer interface { + Begin() chan struct{} + Received() +} + +type BlockingTracer struct { + traces chan struct{} + times chan time.Time + reg registry.Observable + Logger logger.Logger + Trace logger.Logger +} + +func New(reg registry.Observable, options ...func(*BlockingTracer)) *BlockingTracer { + tracer := &BlockingTracer{ + reg: reg, + traces: make(chan struct{}), + times: make(chan time.Time, 1), + Logger: &logger.NoopLogger{}, + } + + for _, option := range options { + option(tracer) + } + + tracer.Trace = tracer.Logger.Level(logger.Trace) + + return tracer +} + +func (tracer *BlockingTracer) Begin() chan struct{} { + tracer.Trace.Printf("beginning trace") + tracer.times <- time.Now() + return tracer.traces +} + +func (tracer *BlockingTracer) Received() { + start := <-tracer.times + duration := float64(time.Since(start).Milliseconds()) + tracer.Trace.With("duration", fmt.Sprintf("%0.2f", duration)).Printf("ending trace") + tracer.reg.Observe(duration) + tracer.traces <- struct{}{} +} + +type NoopTracer struct { + c chan struct{} +} + +func NewNoopTracer() *NoopTracer { + c := make(chan struct{}) + close(c) + return &NoopTracer{ + c: c, + } +} + +func (tracer *NoopTracer) Begin() chan struct{} { + return tracer.c +} + +func (tracer *NoopTracer) Received() {} diff --git a/samples/krill/components/tracer/tracer_test.go b/samples/krill/components/tracer/tracer_test.go new file mode 100644 index 0000000..6117d6d --- /dev/null +++ b/samples/krill/components/tracer/tracer_test.go @@ -0,0 +1,46 @@ +package tracer + +import ( + "testing" + "time" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/logger" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestBlockingTracer(t *testing.T) { + waitMs := 5 + done := make(chan struct{}, 1) + tracer := New(®istry.MockObservable{ + OnObserve: func(val float64) { + // Ensure we were blocked for at least waitMs. + require.GreaterOrEqual(t, int(val), waitMs) + done <- struct{}{} + }, + }, func(bt *BlockingTracer) { + bt.Logger = &logger.NoopLogger{} + }) + + first := tracer.Begin() + go func() { + <-first + done <- struct{}{} + }() + // Block for several milliseconds + <-time.After(time.Millisecond * time.Duration(waitMs)) + tracer.Received() + + <-done + <-done +} + +func TestNoopTracer(t *testing.T) { + tracer := NewNoopTracer() + tracer.Received() + <-tracer.Begin() +} diff --git a/samples/krill/configs/simple/config.yml b/samples/krill/configs/simple/config.yml new file mode 100644 index 0000000..b4105c5 --- /dev/null +++ b/samples/krill/configs/simple/config.yml @@ -0,0 +1,22 @@ +metrics: + type: prometheus + port: 2114 +logLevel: 1 +simulation: + target: + host: localhost + port: 1883 + sites: + - name: site0 + mqttVersion: v3 + assetCount: 3 + tags: + - id: float_1 + configuration: x + count: 1 + rate: + messagesPerPeriod: 2 + periodSeconds: 1 + tagsPerMessage: 2 + payloadFormat: OPCUA + topicFormat: /{{.SiteName}}/{{.AssetName}} diff --git a/samples/krill/go.mod b/samples/krill/go.mod new file mode 100644 index 0000000..84f25e5 --- /dev/null +++ b/samples/krill/go.mod @@ -0,0 +1,51 @@ +module github.com/iot-for-all/device-simulation + +go 1.20 + +require ( + github.com/eclipse/paho.golang v0.11.0 + github.com/eclipse/paho.mqtt.golang v1.4.2 + github.com/gofiber/fiber/v2 v2.42.0 + github.com/magefile/mage v1.15.0 + github.com/princjef/mageutil v1.0.0 + github.com/prometheus/client_golang v1.14.0 + github.com/rs/zerolog v1.29.0 + github.com/stretchr/testify v1.8.2 + google.golang.org/protobuf v1.29.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cheggaaa/pb/v3 v3.1.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect + github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect + github.com/tinylib/msgp v1.1.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.44.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.8.0 // indirect +) diff --git a/samples/krill/go.sum b/samples/krill/go.sum new file mode 100644 index 0000000..f7d3b9a --- /dev/null +++ b/samples/krill/go.sum @@ -0,0 +1,207 @@ +github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheggaaa/pb v2.0.7+incompatible/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/cheggaaa/pb/v3 v3.0.4/go.mod h1:7rgWxLrAUcFMkvJuv09+DYi7mMUYi8nO9iOWcvGJPfw= +github.com/cheggaaa/pb/v3 v3.1.2 h1:FIxT3ZjOj9XJl0U4o2XbEhjFfZl7jCVCDOGq1ZAB7wQ= +github.com/cheggaaa/pb/v3 v3.1.2/go.mod h1:SNjnd0yKcW+kw0brSusraeDd5Bf1zBfxAzTL2ss3yQ4= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eclipse/paho.golang v0.11.0 h1:6Avu5dkkCfcB61/y1vx+XrPQ0oAl4TPYtY0uw3HbQdM= +github.com/eclipse/paho.golang v0.11.0/go.mod h1:rhrV37IEwauUyx8FHrvmXOKo+QRKng5ncoN1vJiJMcs= +github.com/eclipse/paho.mqtt.golang v1.4.2 h1:66wOzfUHSSI1zamx7jR6yMEI5EuHnT1G6rNA5PM12m4= +github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofiber/fiber/v2 v2.42.0 h1:Fnp7ybWvS+sjNQsFvkhf4G8OhXswvB6Vee8hM/LyS+8= +github.com/gofiber/fiber/v2 v2.42.0/go.mod h1:3+SGNjqMh5VQH5Vz2Wdi43zTIV16ktlFd3x3R6O1Zlc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/matryer/is v1.3.0 h1:9qiso3jaJrOe6qBRJRBt2Ldht05qDiFP9le0JOIhRSI= +github.com/matryer/is v1.3.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/princjef/mageutil v1.0.0 h1:1OfZcJUMsooPqieOz2ooLjI+uHUo618pdaJsbCXcFjQ= +github.com/princjef/mageutil v1.0.0/go.mod h1:mkShhaUomCYfAoVvTKRcbAs8YSVPdtezI5j6K+VXhrs= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= +github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4= +github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8= +github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.44.0 h1:R+gLUhldIsfg1HokMuQjdQ5bh9nuXHPIfvkYUu9eR5Q= +github.com/valyala/fasthttp v1.44.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/VividCortex/ewma.v1 v1.1.1/go.mod h1:TekXuFipeiHWiAlO1+wSS23vTcyFau5u3rxXUSXj710= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/cheggaaa/pb.v2 v2.0.7/go.mod h1:0CiZ1p8pvtxBlQpLXkHuUTpdJ1shm3OqCF1QugkjHL4= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fatih/color.v1 v1.7.0/go.mod h1:P7yosIhqIl/sX8J8UypY5M+dDpD2KmyfP5IRs5v/fo0= +gopkg.in/mattn/go-colorable.v0 v0.1.0/go.mod h1:BVJlBXzARQxdi3nZo6f6bnl5yR20/tOL6p+V0KejgSY= +gopkg.in/mattn/go-isatty.v0 v0.0.4/go.mod h1:wt691ab7g0X4ilKZNmMII3egK0bTxl37fEn/Fwbd8gc= +gopkg.in/mattn/go-runewidth.v0 v0.0.4/go.mod h1:BmXejnxvhwdaATwiJbB1vZ2dtXkQKZGu9yLFCZb4msQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/samples/krill/lib/binary/binary.go b/samples/krill/lib/binary/binary.go new file mode 100644 index 0000000..e675fc6 --- /dev/null +++ b/samples/krill/lib/binary/binary.go @@ -0,0 +1,64 @@ +package binary + +import ( + "encoding/binary" + "errors" + "math" + "time" +) + +type Encoder interface { + Encode(a any) ([]byte, error) +} + +type BinaryEncoder struct { + endian binary.ByteOrder +} + +func New(endian binary.ByteOrder) *BinaryEncoder { + return &BinaryEncoder{ + endian: endian, + } +} + +var ( + ErrInvalidFormatType = errors.New("cannot format this type into binary") +) + +func (encoder *BinaryEncoder) Encode(a any) ([]byte, error) { + var bts []byte + switch element := a.(type) { + case float64: + var buffer [8]byte + encoder.endian.PutUint64(buffer[:], math.Float64bits(element)) + bts = append(bts, buffer[:]...) + case int: + var buffer [4]byte + encoder.endian.PutUint32(buffer[:], uint32(element)) + bts = append(bts, buffer[:]...) + case string: + bts = append(bts, []byte(element)...) + case time.Time: + bts = append(bts, []byte(element.String())...) + case []any: + for _, elem := range element { + res, err := encoder.Encode(elem) + if err != nil { + return nil, err + } + bts = append(bts, res...) + } + default: + return nil, ErrInvalidFormatType + } + + return bts, nil +} + +type MockEncoder struct { + OnEncode func(a any) ([]byte, error) +} + +func (encoder *MockEncoder) Encode(a any) ([]byte, error) { + return encoder.OnEncode(a) +} diff --git a/samples/krill/lib/binary/binary_test.go b/samples/krill/lib/binary/binary_test.go new file mode 100644 index 0000000..8d8eed5 --- /dev/null +++ b/samples/krill/lib/binary/binary_test.go @@ -0,0 +1,77 @@ +package binary + +import ( + "encoding/binary" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestBinaryEncoderInt(t *testing.T) { + encoder := New(binary.BigEndian) + bts := make([]byte, 4) + res, err := encoder.Encode(0) + require.NoError(t, err) + require.Equal(t, bts, res) +} + +func TestBinaryEncoderFloat(t *testing.T) { + encoder := New(binary.BigEndian) + bts := make([]byte, 8) + res, err := encoder.Encode(0.0) + require.NoError(t, err) + require.Equal(t, bts, res) +} + +func TestBinaryEncoderString(t *testing.T) { + encoder := New(binary.BigEndian) + bts := []byte{0x41} + res, err := encoder.Encode("A") + require.NoError(t, err) + require.Equal(t, bts, res) +} + +func TestBinaryEncoderTime(t *testing.T) { + encoder := New(binary.BigEndian) + _, err := encoder.Encode(time.Now()) + require.NoError(t, err) +} + +func TestBinaryEncoderArray(t *testing.T) { + encoder := New(binary.BigEndian) + bts := make([]byte, 12) + res, err := encoder.Encode([]any{0, 0.0}) + require.NoError(t, err) + require.Equal(t, bts, res) +} + +func TestBinaryEncoderInvalidType(t *testing.T) { + encoder := New(binary.BigEndian) + type invalid int + _, err := encoder.Encode(invalid(1)) + require.Equal(t, ErrInvalidFormatType, err) +} + +func TestBinaryEncoderInvalidTypeWithinArray(t *testing.T) { + encoder := New(binary.BigEndian) + type invalid int + _, err := encoder.Encode([]any{invalid(1)}) + require.Equal(t, ErrInvalidFormatType, err) +} + +func TestMockEncoder(t *testing.T) { + encoder := &MockEncoder{ + OnEncode: func(a any) ([]byte, error) { + require.Nil(t, a) + return nil, nil + }, + } + + _, err := encoder.Encode(nil) + require.NoError(t, err) +} \ No newline at end of file diff --git a/samples/krill/lib/component/component.go b/samples/krill/lib/component/component.go new file mode 100644 index 0000000..42f3408 --- /dev/null +++ b/samples/krill/lib/component/component.go @@ -0,0 +1,19 @@ +package component + +type ( + ID string +) + +type ( + Store[E any, I comparable] interface { + Create(entity E, identifier I) error + Get(identifier I) (E, error) + Check(identifier I) error + Delete(identifier I) error + List() ([]I, error) + } + + Service[E any, I comparable] interface { + Create(identifier I, entity E) error + } +) \ No newline at end of file diff --git a/samples/krill/lib/component/component_test.go b/samples/krill/lib/component/component_test.go new file mode 100644 index 0000000..faca3dc --- /dev/null +++ b/samples/krill/lib/component/component_test.go @@ -0,0 +1,73 @@ +package component + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + NotFoundErrorMessage = "not found" + MockErrorMessage = "mock" +) + +func TestNotFoundError(t *testing.T) { + err := &NotFoundError{} + require.Equal(t, NotFoundErrorMessage, err.Error()) +} + +func TestMockService(t *testing.T) { + service := &MockService[struct{}, int]{ + OnCreate: func(identifier int, entity struct{}) error { + require.Equal(t, 0, identifier) + require.Equal(t, struct{}{}, entity) + return nil + }, + } + require.NoError(t, service.Create(0, struct{}{})) +} + +func TestMockStore(t *testing.T) { + store := &MockStore[struct{}, int]{ + OnCreate: func(entity struct{}, identifier int) error { + require.Equal(t, 0, identifier) + require.Equal(t, struct{}{}, entity) + return nil + }, + OnGet: func(identifier int) (struct{}, error) { + require.Equal(t, 0, identifier) + return struct{}{}, nil + }, + OnCheck: func(identifier int) error { + require.Equal(t, 0, identifier) + return nil + }, + OnDelete: func(identifier int) error { + require.Equal(t, 0, identifier) + return nil + }, + OnList: func() ([]int, error) { + return nil, nil + }, + } + + require.NoError(t, store.Create(struct{}{}, 0)) + require.NoError(t, store.Check(0)) + require.NoError(t, store.Delete(0)) + + _, err := store.Get(0) + require.NoError(t, err) + + _, err = store.List() + require.NoError(t, err) +} + +func TestMockError(t *testing.T) { + err := &MockError{ + OnError: func() string { + return MockErrorMessage + }, + } + + require.Equal(t, MockErrorMessage, err.Error()) +} \ No newline at end of file diff --git a/samples/krill/lib/component/errors.go b/samples/krill/lib/component/errors.go new file mode 100644 index 0000000..5b1c083 --- /dev/null +++ b/samples/krill/lib/component/errors.go @@ -0,0 +1,7 @@ +package component + +type NotFoundError struct {} + +func (err *NotFoundError) Error() string { + return "not found" +} \ No newline at end of file diff --git a/samples/krill/lib/component/mock.go b/samples/krill/lib/component/mock.go new file mode 100644 index 0000000..d054733 --- /dev/null +++ b/samples/krill/lib/component/mock.go @@ -0,0 +1,46 @@ +package component + +type ( + MockService[E any, I comparable] struct { + OnCreate func(identifier I, entity E) error + } + MockStore[E any, I comparable] struct { + OnCreate func(entity E, identifier I) error + OnGet func(identifier I) (E, error) + OnCheck func(identifier I) error + OnDelete func(identifier I) error + OnList func() ([]I, error) + } +) + +func (service *MockService[E, I]) Create(identifier I, entity E) error { + return service.OnCreate(identifier, entity) +} + +func (store *MockStore[E, I]) Create(entity E, identifier I) error { + return store.OnCreate(entity, identifier) +} + +func (store *MockStore[E, I]) Get(identifier I) (E, error) { + return store.OnGet(identifier) +} + +func (store *MockStore[E, I]) Check(identifier I) error { + return store.OnCheck(identifier) +} + +func (store *MockStore[E, I]) Delete(identifier I) error { + return store.OnDelete(identifier) +} + +func (store *MockStore[E, I]) List() ([]I, error) { + return store.OnList() +} + +type MockError struct { + OnError func() string +} + +func (err *MockError) Error() string { + return err.OnError() +} diff --git a/samples/krill/lib/component/store.go b/samples/krill/lib/component/store.go new file mode 100644 index 0000000..04d9081 --- /dev/null +++ b/samples/krill/lib/component/store.go @@ -0,0 +1,77 @@ +package component + +import ( + "sync" +) + +type StoreOption[E any, I comparable] func(*Memstore[E, I]) + +type Memstore[E any, I comparable] struct { + memory map[I]E + mu *sync.RWMutex +} + +func New[E any, I comparable]() *Memstore[E, I] { + store := &Memstore[E, I]{ + memory: make(map[I]E), + mu: &sync.RWMutex{}, + } + + return store +} + +func (memstore *Memstore[E, I]) Create(entity E, identifier I) error { + + memstore.mu.Lock() + memstore.memory[identifier] = entity + memstore.mu.Unlock() + + return nil +} + +func (memstore *Memstore[E, I]) Get(identifier I) (E, error) { + var entity E + + memstore.mu.RLock() + entity, ok := memstore.memory[identifier] + memstore.mu.RUnlock() + + if ok { + return entity, nil + } + return entity, &NotFoundError{} +} + +func (memstore *Memstore[E, I]) Check(identifier I) error { + memstore.mu.RLock() + _, ok := memstore.memory[identifier] + memstore.mu.RUnlock() + + if ok { + return nil + } + return &NotFoundError{} +} + +func (memstore *Memstore[E, I]) Delete(identifier I) error { + + memstore.mu.Lock() + delete(memstore.memory, identifier) + memstore.mu.Unlock() + return nil +} + +func (memstore *Memstore[E, I]) List() ([]I, error) { + keys := make([]I, len(memstore.memory)) + + index := 0 + + memstore.mu.RLock() + for k := range memstore.memory { + keys[index] = k + index++ + } + memstore.mu.RUnlock() + + return keys, nil +} diff --git a/samples/krill/lib/component/store_test.go b/samples/krill/lib/component/store_test.go new file mode 100644 index 0000000..469d447 --- /dev/null +++ b/samples/krill/lib/component/store_test.go @@ -0,0 +1,71 @@ +package component + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +var ( + keyOne = "1" + valueOne = 1 + keyTwo = "2" + valueTwo = 2 +) + +func TestCreate(t *testing.T) { + store := New[int, string]() + err := store.Create(valueOne, keyOne) + require.NoError(t, err) +} + +func TestGet(t *testing.T) { + store := New[int, string]() + err := store.Create(valueOne, keyOne) + require.NoError(t, err) + v, err := store.Get(keyOne) + require.NoError(t, err) + require.Equal(t, valueOne, v) +} + +func TestDelete(t *testing.T) { + store := New[int, string]() + err := store.Delete(keyOne) + require.NoError(t, err) + err = store.Create(valueOne, keyOne) + require.NoError(t, err) + v, err := store.Get(keyOne) + require.NoError(t, err) + require.Equal(t, valueOne, v) + err = store.Delete(keyOne) + require.NoError(t, err) + _, err = store.Get(keyOne) + require.Equal(t, &NotFoundError{}, err) +} + +func TestList(t *testing.T) { + store := New[int, string]() + _, err := store.List() + require.NoError(t, err) + err = store.Create(valueOne, keyOne) + require.NoError(t, err) + err = store.Create(valueTwo, keyTwo) + require.NoError(t, err) + vals, err := store.List() + require.NoError(t, err) + require.ElementsMatch(t, []string{keyOne, keyTwo}, vals) +} + +func TestCheck(t *testing.T) { + store := New[int, string]() + err := store.Check(keyOne) + require.Equal(t, &NotFoundError{}, err) + err = store.Create(valueOne, keyOne) + require.NoError(t, err) + err = store.Check(keyOne) + require.NoError(t, err) +} diff --git a/samples/krill/lib/composition/composition.go b/samples/krill/lib/composition/composition.go new file mode 100644 index 0000000..1924386 --- /dev/null +++ b/samples/krill/lib/composition/composition.go @@ -0,0 +1,179 @@ +package composition + +import ( + "sort" + + "github.com/iot-for-all/device-simulation/lib/expression" + "github.com/iot-for-all/device-simulation/lib/logger" +) + +const ( + ExpressionErrorMessage = "an error occurred while evaluating the expression leaf" +) + +type Renderer interface { + Render(map[string]any) any +} + +type Node interface { + Renderer + With(Edge) Node +} + +type Edge interface { + Renderer + Edge() any +} + +// Edge implementations. +type Label struct { + label string + value Renderer +} + +func NewLabel(label string, value Renderer) *Label { + return &Label{ + label: label, + value: value, + } +} + +func (label *Label) Render(env map[string]any) any { + return label.value.Render(env) +} + +func (label *Label) Edge() any { + return label.label +} + +type Position struct { + position int + value Renderer +} + +func NewPosition(position int, value Renderer) *Position { + return &Position{ + position: position, + value: value, + } +} + +func (position *Position) Render(env map[string]any) any { + return position.value.Render(env) +} + +func (position *Position) Edge() any { + return position.position +} + +// Node implementations. +type Collection struct { + labels []*Label +} + +func NewCollection() *Collection { + return &Collection{} +} + +func (collection *Collection) With(e Edge) Node { + collection.labels = append(collection.labels, e.(*Label)) + return collection +} + +func (collection *Collection) Render(env map[string]any) any { + m := make(map[string]any) + for _, label := range collection.labels { + switch r := label.Render(env).(type) { + case Renderer: + m[label.Edge().(string)] = r.Render(env) + default: + m[label.Edge().(string)] = r + } + } + return m +} + +type ArrayPositions []*Position + +func (a ArrayPositions) Len() int { + return len(a) +} + +func (a ArrayPositions) Swap(i int, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ArrayPositions) Less(i int, j int) bool { + return a[i].Edge().(int) < a[j].Edge().(int) +} + +type Array struct { + Positions ArrayPositions +} + +func NewArray() *Array { + return &Array{} +} + +func (array *Array) With(e Edge) Node { + array.Positions = append(array.Positions, e.(*Position)) + return array +} + +func (array *Array) Render(env map[string]any) any { + a := make([]any, len(array.Positions)) + + sort.Sort(array.Positions) + + for idx, position := range array.Positions { + a[idx] = position.Render(env) + } + + return a +} + +// Renderer implementations. +type Expression struct { + Logger logger.Logger + evaluator expression.Evaluator +} + +func NewExpression( + evaluator expression.Evaluator, + options ...func(*Expression), +) *Expression { + expr := &Expression{ + evaluator: evaluator, + Logger: &logger.NoopLogger{}, + } + + for _, option := range options { + option(expr) + } + + return expr +} + +func (expr *Expression) Render(ctx map[string]any) any { + v, err := expr.evaluator.Evaluate(ctx) + if err != nil { + expr.Logger.Level(logger.Error). + With("error", err.Error()). + Printf(ExpressionErrorMessage) + } + return v +} + +type Static struct { + value any +} + +func NewStatic(value any) *Static { + return &Static{ + value: value, + } +} + +func (stat *Static) Render(map[string]any) any { + return stat.value +} diff --git a/samples/krill/lib/composition/composition_test.go b/samples/krill/lib/composition/composition_test.go new file mode 100644 index 0000000..cb6b235 --- /dev/null +++ b/samples/krill/lib/composition/composition_test.go @@ -0,0 +1,128 @@ +package composition + +import ( + "errors" + "testing" + + "github.com/iot-for-all/device-simulation/lib/expression" + "github.com/iot-for-all/device-simulation/lib/logger" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestLabelCollectionPositionAndStatic(t *testing.T) { + expected := map[string]any{ + "my-label": map[string]any{ + "my-array": []any{5, 6}, + }, + } + + val := NewCollection().With( + NewLabel("my-label", NewCollection().With( + NewLabel("my-array", NewArray().With( + NewPosition(5, NewStatic(5)), + ).With( + NewPosition(6, NewStatic(6)), + )), + )), + ).Render(nil) + + require.Equal(t, expected, val) +} + +func TestExpression(t *testing.T) { + expected := 1 + expr := NewExpression(&expression.MockEvaluator{ + OnEvaluate: func(m map[string]any) (any, error) { + return expected, nil + }, + }, func(e *Expression) { + e.Logger = &logger.MockLogger{ + OnPrintf: func(s string, i ...interface{}) { + require.Equal(t, ExpressionErrorMessage, s) + }, OnLevel: func(i int) logger.Logger { + require.Equal(t, logger.Error, i) + return e.Logger + }, OnWith: func(s1, s2 string) logger.Logger { + return e.Logger + }, + } + }) + + res := expr.Render(nil) + require.Equal(t, expected, res) +} + +func TestExpressionErrorLog(t *testing.T) { + errMock := errors.New("mock error") + expr := NewExpression(&expression.MockEvaluator{ + OnEvaluate: func(m map[string]any) (any, error) { + return nil, errMock + }, + }, func(e *Expression) { + e.Logger = &logger.MockLogger{ + OnPrintf: func(s string, i ...interface{}) { + require.Equal(t, ExpressionErrorMessage, s) + }, OnLevel: func(i int) logger.Logger { + require.Equal(t, logger.Error, i) + return e.Logger + }, OnWith: func(s1, s2 string) logger.Logger { + require.Equal(t, errMock.Error(), s2) + return e.Logger + }, + } + }) + + expr.Render(nil) +} + +func TestSwapArrayPositions(t *testing.T) { + pos := ArrayPositions{ + { + position: 0, + }, + { + position: 1, + }, + } + + pos.Swap(0, 1) + + require.Equal(t, 1, pos[0].position) + require.Equal(t, 0, pos[1].position) +} + +func TestMockRenderer(t *testing.T) { + renderer := &MockRenderer{ + OnRender: func(m map[string]any) any { + require.Nil(t, m) + return nil + }, + } + + require.Nil(t, renderer.Render(nil)) +} + +func TestMockEdge(t *testing.T) { + edge := &MockEdge{ + OnEdge: func() any { + return nil + }, + } + + require.Nil(t, edge.Edge()) +} + +func TestMockNode(t *testing.T) { + node := &MockNode{ + OnWith: func(e Edge) Node { + require.Equal(t, &MockEdge{}, e) + return &MockNode{} + }, + } + + require.Equal(t, &MockNode{}, node.With(&MockEdge{})) +} \ No newline at end of file diff --git a/samples/krill/lib/composition/mock.go b/samples/krill/lib/composition/mock.go new file mode 100644 index 0000000..02cb074 --- /dev/null +++ b/samples/krill/lib/composition/mock.go @@ -0,0 +1,27 @@ +package composition + +type MockRenderer struct { + OnRender func(map[string]any) any +} + +func (renderer *MockRenderer) Render(m map[string]any) any { + return renderer.OnRender(m) +} + +type MockEdge struct { + Renderer + OnEdge func() any +} + +func (edge *MockEdge) Edge() any { + return edge.OnEdge() +} + +type MockNode struct { + Renderer + OnWith func(Edge) Node +} + +func (node *MockNode) With(e Edge) Node { + return node.OnWith(e) +} \ No newline at end of file diff --git a/samples/krill/lib/counter/counter.go b/samples/krill/lib/counter/counter.go new file mode 100644 index 0000000..5278659 --- /dev/null +++ b/samples/krill/lib/counter/counter.go @@ -0,0 +1,145 @@ +// Package counter contains the counter-based implementations of a Provider. +// Counter Providers represent and wrap the counterVec prometheus metrics. +package counter + +import ( + "fmt" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/errors" + + "github.com/prometheus/client_golang/prometheus" +) + +// Provider is an implementation of the registry.Provider interface. +// Its purpose is to register a new prometheus counterVec when created, +// And to create a counter (a prometheus counter with a provided label) when its with function is called. +type Provider struct { + CounterVec *prometheus.CounterVec + registry prometheus.Registerer + Name string + Help string + Label string +} + +const ( + SimulationCounterDefaultName = "simulation_counter" + SimulationCounterDefaultHelp = "Simulation counter" + CounterLabelKey = "counter" +) + +type InvalidPrometheusCounterVecNameError struct { + errors.BadRequest + name string +} + +func (err *InvalidPrometheusCounterVecNameError) Error() string { + return fmt.Sprintf( + "could not create the counter provider with the name %s because the name has already been registered or is invalid", + err.name, + ) +} + +type InvalidPrometheusCounterLabelError struct { + errors.BadRequest + name string + label string +} + +func (err *InvalidPrometheusCounterLabelError) Error() string { + return fmt.Sprintf( + "could not create the prometheus counter with label %s from counter provider %s because the label has already been used or is invalid", + err.label, + err.name, + ) +} + +// New creates a Provider given a prometheus registerer. +// It can also take a function to set optional parameters. +func New( + reg prometheus.Registerer, + options ...func(*Provider), +) (*Provider, error) { + counterProvider := &Provider{ + Name: SimulationCounterDefaultName, + Help: SimulationCounterDefaultHelp, + Label: CounterLabelKey, + registry: reg, + } + + for _, option := range options { + option(counterProvider) + } + + counterProvider.CounterVec = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: counterProvider.Name, + Help: counterProvider.Help, + }, + []string{counterProvider.Label}, + ) + + err := counterProvider.registry.Register(counterProvider.CounterVec) + + if err != nil { + return nil, &InvalidPrometheusCounterVecNameError{ + name: counterProvider.Name, + } + } + + return counterProvider, nil +} + +// Cancel unregisters the prometheus counterVec from the prometheus registerer. +func (counterProvider *Provider) Cancel() error { + counterProvider.registry.Unregister(counterProvider.CounterVec) + return nil +} + +// With attempts to create a Counter, which is a wrapper around the prometheus counter metric that implements the CancellableObservable interface. +// It returns an error if a failure is encountered. +func (counterProvider *Provider) With( + label string, +) (registry.CancellableObservable, error) { + + counter, err := counterProvider.CounterVec.GetMetricWith( + prometheus.Labels{counterProvider.Label: label}, + ) + + if err != nil { + return nil, &InvalidPrometheusCounterLabelError{ + name: counterProvider.Name, + label: label, + } + } + + return NewCounter(counterProvider.registry, counter), nil +} + +// Counter is an implementation of CancellableObservable which wraps the functionality of the prometheus counter metric. +// It increments the counter when its observe function is called. +type Counter struct { + observable prometheus.Counter + registry prometheus.Registerer +} + +// NewCounter creates a Counter given a prometheus counter and a prometheus registerer. +func NewCounter( + reg prometheus.Registerer, + observable prometheus.Counter, +) *Counter { + return &Counter{ + registry: reg, + observable: observable, + } +} + +// Cancel unregisters the prometheus counter from the registerer. +func (counter Counter) Cancel() { + counter.registry.Unregister(counter.observable) +} + +// Observe calls the increment function of the prometheus counter. +func (counter Counter) Observe(float64) { + counter.observable.Inc() +} \ No newline at end of file diff --git a/samples/krill/lib/counter/counter_test.go b/samples/krill/lib/counter/counter_test.go new file mode 100644 index 0000000..745b0ca --- /dev/null +++ b/samples/krill/lib/counter/counter_test.go @@ -0,0 +1,104 @@ +package counter + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +const ( + name = "name" + label = "label" + invalid = "{}|}][" + invalidUTF8 = "\xc3\x28" +) + +func TestSimpleCounterProvider(t *testing.T) { + registry := prometheus.NewRegistry() + + counterProvider, err := New(registry) + + require.NoError(t, err) + + counterProvider.Cancel() +} + +func TestCounterProviderWithInvalidName(t *testing.T) { + registry := prometheus.NewRegistry() + + _, err := New(registry, func(cp *Provider) { + cp.Name = invalid + }) + + require.Equal(t, &InvalidPrometheusCounterVecNameError{ + name: invalid, + }, err) +} + +func TestCounterProviderWithDuplicateNames(t *testing.T) { + + registry := prometheus.NewRegistry() + + counterProvider, err := New(registry, func(cp *Provider) { + cp.Name = name + }) + + require.NoError(t, err) + + _, err = counterProvider.With(label) + + require.NoError(t, err) + + _, err = New(registry, func(cp *Provider) { + cp.Name = name + }) + + require.Equal(t, (&InvalidPrometheusCounterVecNameError{ + name: name, + }).Error(), err.Error()) +} + +func TestCounterWithLabel(t *testing.T) { + + registry := prometheus.NewRegistry() + + counterProvider, err := New(registry, func(cp *Provider) { + cp.Name = name + }) + + require.NoError(t, err) + + counter, err := counterProvider.With(name) + + counter.Observe(0) + counter.Cancel() + + require.NoError(t, err) +} + +func TestCounterWithLabelError(t *testing.T) { + + registry := prometheus.NewRegistry() + + counterProvider, err := New(registry, func(cp *Provider) { + cp.Name = name + }) + + require.NoError(t, err) + + _, err = counterProvider.With("") + + require.NoError(t, err) + + _, err = counterProvider.With(invalidUTF8) + + require.Equal(t, (&InvalidPrometheusCounterLabelError{ + name: name, + label: invalidUTF8, + }).Error(), err.Error()) +} \ No newline at end of file diff --git a/samples/krill/lib/dialer/dialer.go b/samples/krill/lib/dialer/dialer.go new file mode 100644 index 0000000..6d7ca14 --- /dev/null +++ b/samples/krill/lib/dialer/dialer.go @@ -0,0 +1,147 @@ +// Package dialers defines interfaces for GRPC and TCP dialers which are used across various parts of the simulation framework. +// It also contains mock implementations for testing purposes and wrappers around the dial functions provided by the net and google grpc packages. +package dialer + +import ( + "net" + "time" +) + +// Dialer is an interface which describes the net package's dialer functionality. +type Dialer interface { + Dial(network string, address string) (net.Conn, error) +} + +// NetDialer is an implementation of Dialer which wraps the dial functionality of the net package dial function. +type NetDialer struct{ + OnDial func(network string, address string) (net.Conn, error) +} + +func New(options ...func(*NetDialer)) *NetDialer { + dialer := &NetDialer{ + OnDial: net.Dial, + } + + for _, option := range options { + option(dialer) + } + + return dialer +} + +// Dial calls the net package's dial function, passing through its network type and address parameters. +func (dialer *NetDialer) Dial(network string, address string) (net.Conn, error) { + return dialer.OnDial(network, address) +} + +type MockDialer struct { + OnDial func(network string, address string) (net.Conn, error) +} + +func (dialer *MockDialer) Dial(network string, address string) (net.Conn, error) { + return dialer.OnDial(network, address) +} + +type MockConn struct { + OnRead func(b []byte) (n int, err error) + OnWrite func(b []byte) (n int, err error) + OnClose func() error + OnLocalAddr func() net.Addr + OnRemoteAddr func() net.Addr + OnSetDeadline func(t time.Time) error + OnSetReadDeadline func(t time.Time) error + OnSetWriteDeadline func(t time.Time) error +} + +func (conn *MockConn) Read(b []byte) (n int, err error) { + return conn.OnRead(b) +} + +func (conn *MockConn) Write(b []byte) (n int, err error) { + return conn.OnWrite(b) +} + +func (conn *MockConn) Close() error { + return conn.OnClose() +} + +func (conn *MockConn) LocalAddr() net.Addr { + return conn.OnLocalAddr() +} + +func (conn *MockConn) RemoteAddr() net.Addr { + return conn.OnRemoteAddr() +} + +func (conn *MockConn) SetDeadline(t time.Time) error { + return conn.OnSetDeadline(t) +} + +func (conn *MockConn) SetReadDeadline(t time.Time) error { + return conn.OnSetReadDeadline(t) +} + +func (conn *MockConn) SetWriteDeadline(t time.Time) error { + return conn.OnSetWriteDeadline(t) +} + +type MockAddr struct { + OnNetwork func() string + OnString func() string +} + +func (addr *MockAddr) Network() string { + return addr.OnNetwork() +} + +func (addr *MockAddr) String() string { + return addr.OnString() +} + +type NoopConn struct { + +} + +func (*NoopConn) Read(b []byte) (n int, err error) { + return 0, nil +} + +func (*NoopConn) Write(b []byte) (n int, err error) { + return 0, nil +} + +func (*NoopConn) Close() error { + return nil +} + +func (*NoopConn) LocalAddr() net.Addr { + return &NoopAddr{} +} + +func (*NoopConn) RemoteAddr() net.Addr { + return &NoopAddr{} +} + +func (*NoopConn) SetDeadline(t time.Time) error { + return nil +} + +func (*NoopConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (*NoopConn) SetWriteDeadline(t time.Time) error { + return nil +} + +type NoopAddr struct { + +} + +func (*NoopAddr) Network() string { + return "" +} + +func (*NoopAddr) String() string { + return "" +} \ No newline at end of file diff --git a/samples/krill/lib/dialer/dialer_test.go b/samples/krill/lib/dialer/dialer_test.go new file mode 100644 index 0000000..86b78e8 --- /dev/null +++ b/samples/krill/lib/dialer/dialer_test.go @@ -0,0 +1,121 @@ +package dialer + +import ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const ( + MockString = "MockString" + MockNetwork = "MockNetwork" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestMockConn(t *testing.T) { + conn := &MockConn{ + OnRead: func(b []byte) (int, error) { + return 0, nil + }, + OnWrite: func(b []byte) (int, error) { + return 0, nil + }, + OnClose: func() error { + return nil + }, + OnLocalAddr: func() net.Addr { + return nil + }, + OnRemoteAddr: func() net.Addr { + return nil + }, + OnSetDeadline: func(t time.Time) error { + return nil + }, + OnSetReadDeadline: func(t time.Time) error { + return nil + }, + OnSetWriteDeadline: func(t time.Time) error { + return nil + }, + } + + require.NoError(t, conn.Close()) + require.NoError(t, conn.SetDeadline(time.Now())) + require.NoError(t, conn.SetReadDeadline(time.Now())) + require.NoError(t, conn.SetWriteDeadline(time.Now())) + + _, err := conn.Read(nil) + require.NoError(t, err) + + _, err = conn.Write(nil) + require.NoError(t, err) + + require.Nil(t, conn.LocalAddr()) + require.Nil(t, conn.RemoteAddr()) +} + +func TestMockAddr(t *testing.T) { + addr := &MockAddr{ + OnNetwork: func() string { + return MockNetwork + }, OnString: func() string { + return MockString + }, + } + + require.Equal(t, MockString, addr.String()) + require.Equal(t, MockNetwork, addr.Network()) +} + +func TestNoopConn(t *testing.T) { + conn := &NoopConn{} + + require.NoError(t, conn.Close()) + require.NoError(t, conn.SetDeadline(time.Now())) + require.NoError(t, conn.SetReadDeadline(time.Now())) + require.NoError(t, conn.SetWriteDeadline(time.Now())) + + _, err := conn.Read(nil) + require.NoError(t, err) + + _, err = conn.Write(nil) + require.NoError(t, err) + + require.Equal(t, &NoopAddr{}, conn.LocalAddr()) + require.Equal(t, &NoopAddr{}, conn.RemoteAddr()) +} + +func TestNoopAddr(t *testing.T) { + addr := &NoopAddr{} + + require.Equal(t, "", addr.String()) + require.Equal(t, "", addr.Network()) +} + +func TestNetDialer(t *testing.T) { + dialer := New(func(nd *NetDialer) { + nd.OnDial = func(network, address string) (net.Conn, error) { + return nil, nil + } + }) + + _, err := dialer.Dial("", "") + require.NoError(t, err) +} + +func TestMockDialer(t *testing.T) { + dialer := &MockDialer{ + OnDial: func(network, address string) (net.Conn, error) { + return nil, nil + }, + } + + _, err := dialer.Dial("", "") + require.NoError(t, err) +} \ No newline at end of file diff --git a/samples/krill/lib/env/env.go b/samples/krill/lib/env/env.go new file mode 100644 index 0000000..a825aed --- /dev/null +++ b/samples/krill/lib/env/env.go @@ -0,0 +1,98 @@ +package env + +import ( + "flag" + "os" + "path" + + "gopkg.in/yaml.v3" +) + +type UnmarshalFunc func(data []byte, v any) error + +type ConfigurationReader[E any] struct { + ReadFile func(name string) ([]byte, error) + Unmarshal UnmarshalFunc +} + +func New[E any](options ...func(*ConfigurationReader[E])) *ConfigurationReader[E] { + reader := &ConfigurationReader[E]{ + ReadFile: os.ReadFile, + Unmarshal: yaml.Unmarshal, + } + + for _, option := range options { + option(reader) + } + + return reader +} + +func (reader *ConfigurationReader[E]) Read( + configSrc string, +) (E, error) { + var config E + + content, err := reader.ReadFile(path.Clean(configSrc)) + if err != nil { + return config, &CannotOpenConfigurationFileError{ + err: err, + } + } + + err = reader.Unmarshal(content, &config) + if err != nil { + return config, &CannotParseFileContentError{ + err: err, + } + } + + return config, err +} + +func ReadEnv(key string) string { + return os.Getenv(key) +} + +type FlagParser struct { + ParseInt func(name string, value int, usage string) *int + ParseString func(name string, value string, usage string) *string + ParseBool func(name string, value bool, usage string) *bool + Parse func() +} + +func NewFlagParser(options ...func(*FlagParser)) *FlagParser { + parser := &FlagParser{ + ParseInt: flag.Int, + ParseString: flag.String, + ParseBool: flag.Bool, + Parse: flag.Parse, + } + + for _, option := range options { + option(parser) + } + + return parser +} + +func (parser *FlagParser) ReadFlags(flags map[string]any) (map[string]any, error) { + m := make(map[string]any) + + for f, t := range flags { + switch def := t.(type) { + case int: + m[f] = parser.ParseInt(f, def, "") + case string: + m[f] = parser.ParseString(f, def, "") + case bool: + m[f] = parser.ParseBool(f, def, "") + default: + return nil, &InvalidFlagTypeError{} + } + } + + parser.Parse() + + return m, nil +} diff --git a/samples/krill/lib/env/env_test.go b/samples/krill/lib/env/env_test.go new file mode 100644 index 0000000..55a9186 --- /dev/null +++ b/samples/krill/lib/env/env_test.go @@ -0,0 +1,166 @@ +package env + +import ( + "flag" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + MockSource = "MockSource" + MockConfigProperty = "MockConfig" + MockBoolFlagName = "MockBoolFlagName" + MockStringFlagName = "MockStringFlagName" + MockIntFlagName = "MockIntFlagName" + MockStringFlagDefault = "MockStringFlagDefault" + MockBoolFlagDefault = true + MockIntFlagDefault = 1 +) + +var ( + MockContent = []byte{0, 1} +) + +type MockError struct{} + +func (*MockError) Error() string { + return "mock" +} + +type MockConfig struct { + Property string +} + +var originalFlagSet flag.FlagSet + +func TestMain(m *testing.M) { + originalFlagSet = *flag.CommandLine + m.Run() +} + +func TestReadEnv(t *testing.T) { + v := "test" + err := os.Setenv(v, v) + require.NoError(t, err) + require.Equal(t, v, ReadEnv(v)) + ReadEnv(v) +} + +func TestConfigurationReader(t *testing.T) { + reader := New[MockConfig](func(cr *ConfigurationReader[MockConfig]) { + cr.Unmarshal = func(data []byte, v any) error { + require.Equal(t, MockContent, data) + res, ok := v.(*MockConfig) + require.True(t, ok) + res.Property = MockConfigProperty + return nil + } + cr.ReadFile = func(name string) ([]byte, error) { + require.Equal(t, MockSource, name) + return MockContent, nil + } + }) + + res, err := reader.Read(MockSource) + require.NoError(t, err) + require.Equal(t, MockConfigProperty, res.Property) +} + +func TestConfigurationReaderReadFileError(t *testing.T) { + reader := New[MockConfig](func(cr *ConfigurationReader[MockConfig]) { + cr.ReadFile = func(name string) ([]byte, error) { + return nil, &MockError{} + } + }) + + _, err := reader.Read(MockSource) + require.Equal(t, (&CannotOpenConfigurationFileError{ + err: &MockError{}, + }).Error(), err.Error()) +} + +func TestConfigurationReaderUnmarshalError(t *testing.T) { + reader := New[MockConfig](func(cr *ConfigurationReader[MockConfig]) { + cr.ReadFile = func(name string) ([]byte, error) { + return nil, nil + } + cr.Unmarshal = func(data []byte, v any) error { + return &MockError{} + } + }) + + _, err := reader.Read(MockSource) + require.Equal(t, (&CannotParseFileContentError{ + err: &MockError{}, + }).Error(), err.Error()) +} + +func TestFlagParserBool(t *testing.T) { + done := make(chan struct{}) + parser := NewFlagParser(func(fp *FlagParser) { + fp.Parse = func() { + close(done) + } + fp.ParseBool = func(name string, value bool, usage string) *bool { + require.Equal(t, "", usage) + require.Equal(t, MockBoolFlagName, name) + require.Equal(t, MockBoolFlagDefault, value) + return nil + } + }) + + m, err := parser.ReadFlags(map[string]any{ + MockBoolFlagName: MockBoolFlagDefault, + }) + require.NoError(t, err) + require.Nil(t, m[MockBoolFlagName]) + + <-done +} + +func TestFlagParserInt(t *testing.T) { + parser := NewFlagParser(func(fp *FlagParser) { + fp.Parse = func() {} + fp.ParseInt = func(name string, value int, usage string) *int { + require.Equal(t, "", usage) + require.Equal(t, MockIntFlagName, name) + require.Equal(t, MockIntFlagDefault, value) + return nil + } + }) + + m, err := parser.ReadFlags(map[string]any{ + MockIntFlagName: MockIntFlagDefault, + }) + require.NoError(t, err) + require.Nil(t, m[MockIntFlagName]) +} + +func TestFlagParserString(t *testing.T) { + parser := NewFlagParser(func(fp *FlagParser) { + fp.Parse = func() {} + fp.ParseString = func(name string, value string, usage string) *string { + require.Equal(t, "", usage) + require.Equal(t, MockStringFlagDefault, name) + require.Equal(t, MockStringFlagDefault, value) + return nil + } + }) + + m, err := parser.ReadFlags(map[string]any{ + MockStringFlagDefault: MockStringFlagDefault, + }) + require.NoError(t, err) + require.Nil(t, m[MockStringFlagDefault]) +} + +func TestInvalidFlagTypeError(t *testing.T) { + parser := NewFlagParser(func(fp *FlagParser) {}) + + _, err := parser.ReadFlags(map[string]any{ + "": nil, + }) + require.Equal(t, (&InvalidFlagTypeError{}).Error(), err.Error()) +} diff --git a/samples/krill/lib/env/errors.go b/samples/krill/lib/env/errors.go new file mode 100644 index 0000000..da7c868 --- /dev/null +++ b/samples/krill/lib/env/errors.go @@ -0,0 +1,25 @@ +package env + +import "fmt" + +type CannotOpenConfigurationFileError struct { + err error +} + +func (err *CannotOpenConfigurationFileError) Error() string { + return fmt.Sprintf("the file at the provided path could not be opened: %q", err.err.Error()) +} + +type CannotParseFileContentError struct { + err error +} + +func (err *CannotParseFileContentError) Error() string { + return fmt.Sprintf("the content of the specified file could not be parsed: %q", err.err.Error()) +} + +type InvalidFlagTypeError struct {} + +func (*InvalidFlagTypeError) Error() string { + return "flag default must be of type int, string, or bool" +} \ No newline at end of file diff --git a/samples/krill/lib/environment/environment.go b/samples/krill/lib/environment/environment.go new file mode 100644 index 0000000..6240a05 --- /dev/null +++ b/samples/krill/lib/environment/environment.go @@ -0,0 +1,46 @@ +package environment + +import "sync" + +type Environment interface { + Env() map[string]any + Set(string, any) +} + +type MapEnvironment struct { + env map[string]any + mu sync.RWMutex +} + +func New() *MapEnvironment { + return &MapEnvironment{ + env: make(map[string]any), + } +} + +func (env *MapEnvironment) Env() map[string]any { + env.mu.RLock() + defer env.mu.RUnlock() + + return env.env +} + +func (env *MapEnvironment) Set(s string, a any) { + env.mu.Lock() + defer env.mu.Unlock() + + env.env[s] = a +} + +type MockEnvironment struct { + OnEnv func() map[string]any + OnSet func(string, any) +} + +func (environment *MockEnvironment) Env() map[string]any { + return environment.OnEnv() +} + +func (environment *MockEnvironment) Set(s string, a any) { + environment.OnSet(s, a) +} diff --git a/samples/krill/lib/environment/environment_test.go b/samples/krill/lib/environment/environment_test.go new file mode 100644 index 0000000..ed88229 --- /dev/null +++ b/samples/krill/lib/environment/environment_test.go @@ -0,0 +1,30 @@ +package environment + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestEnvironment(t *testing.T) { + expected := map[string]any{"": 1} + env := New() + env.Set("", 1) + require.Equal(t, expected, env.Env()) +} + +func TestMockEnvironment(t *testing.T) { + env := &MockEnvironment{ + OnEnv: func() map[string]any { + return map[string]any{"": ""} + }, OnSet: func(s string, a any) { + require.Equal(t, s, a) + }, + } + require.Equal(t, "", env.Env()[""]) + env.Set("", "") +} diff --git a/samples/krill/lib/errors/errors.go b/samples/krill/lib/errors/errors.go new file mode 100644 index 0000000..0b4e5cb --- /dev/null +++ b/samples/krill/lib/errors/errors.go @@ -0,0 +1,49 @@ +package errors + +type Category int + +type Error interface { + error + Code() Category +} + +const ( + MOCK Category = iota + BAD_REQUEST + NOT_FOUND +) + +type Custom struct { + code Category + message string +} + +func (c Custom) Code() Category { + return c.code +} + +func (c Custom) Error() string { + return c.message +} + +type Mock struct{} + +func (Mock) Code() Category { + return MOCK +} + +func (Mock) Error() string { + return "mock" +} + +type BadRequest struct{} + +func (BadRequest) Code() Category { + return BAD_REQUEST +} + +type NotFound struct{} + +func (NotFound) Code() Category { + return NOT_FOUND +} diff --git a/samples/krill/lib/errors/errors_test.go b/samples/krill/lib/errors/errors_test.go new file mode 100644 index 0000000..9feec7c --- /dev/null +++ b/samples/krill/lib/errors/errors_test.go @@ -0,0 +1,143 @@ +package errors + +import ( + "net/http" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/iot-for-all/device-simulation/lib/logger" + "github.com/stretchr/testify/require" +) + +const ( + MockErrorCode = 507 + MockErrorCodeString = "507" + MockErrorMessage = "MockErrorMessage" + MockBadRequestString = "400" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestMockError(t *testing.T) { + err := &Mock{} + require.Equal(t, MOCK, err.Code()) + require.Equal(t, "mock", err.Error()) +} + +func TestBadRequest(t *testing.T) { + res := &BadRequest{} + require.Equal(t, BAD_REQUEST, res.Code()) +} + +func TestNotFound(t *testing.T) { + res := &NotFound{} + require.Equal(t, NOT_FOUND, res.Code()) +} + +func TestFiberErrorHandlerFiberError(t *testing.T) { + handler := New(func(feh *FiberErrorHandler) { + feh.Logger = &logger.MockLogger{ + OnLevel: func(i int) logger.Logger { + require.Equal(t, logger.Error, i) + return feh.Logger + }, OnWith: func(s1, s2 string) logger.Logger { + require.Equal(t, "code", s1) + require.Equal(t, MockErrorCodeString, s2) + return feh.Logger + }, OnPrintf: func(s string, i ...interface{}) { + require.Equal(t, "an internal error occurred", s) + }, + } + }) + + err := handler.HandleError(&MockContext{ + OnStatus: func(status int) Context { + require.Equal(t, MockErrorCode, status) + return &MockContext{ + OnSend: func(body []byte) error { + require.Equal(t, []byte(MockErrorMessage), body) + return nil + }, + } + }, + }, &fiber.Error{ + Code: MockErrorCode, + Message: MockErrorMessage, + }) + require.NoError(t, err) +} + +func TestFiberErrorHandlerInternalError(t *testing.T) { + handler := New(func(feh *FiberErrorHandler) { + feh.Logger = &logger.MockLogger{ + OnLevel: func(i int) logger.Logger { + require.Equal(t, logger.Debug, i) + return feh.Logger + }, OnWith: func(s1, s2 string) logger.Logger { + require.Equal(t, "code", s1) + require.Equal(t, MockBadRequestString, s2) + return feh.Logger + }, OnPrintf: func(s string, i ...interface{}) { + require.Equal(t, "an non-500-level error occurred", s) + }, + } + }) + + err := handler.HandleError(&MockContext{ + OnStatus: func(status int) Context { + require.Equal(t, http.StatusBadRequest, status) + return &MockContext{ + OnSend: func(body []byte) error { + require.Equal(t, []byte(MockErrorMessage), body) + return nil + }, + } + }, + }, &Custom{ + code: BAD_REQUEST, + message: MockErrorMessage, + }) + require.NoError(t, err) +} + +func TestFiberErrorHandlerSendError(t *testing.T) { + handler := New(func(feh *FiberErrorHandler) { + feh.Logger = &logger.MockLogger{ + OnLevel: func(i int) logger.Logger { + return feh.Logger + }, OnWith: func(s1, s2 string) logger.Logger { + return feh.Logger + }, OnPrintf: func(s string, i ...interface{}) { + feh.Logger = &logger.MockLogger{ + OnLevel: func(i int) logger.Logger { + require.Equal(t, logger.Error, i) + return feh.Logger + }, OnWith: func(s1, s2 string) logger.Logger { + require.Equal(t, "error", s1) + require.Equal(t, (Mock{}).Error(), s2) + return feh.Logger + }, OnPrintf: func(s string, i ...interface{}) { + require.Equal(t, "error occurred when handling error", s) + }, + } + }, + } + }) + + err := handler.HandleError(&MockContext{ + OnStatus: func(status int) Context { + require.Equal(t, http.StatusBadRequest, status) + return &MockContext{ + OnSend: func(body []byte) error { + return Mock{} + }, + } + }, + }, &Custom{ + code: BAD_REQUEST, + message: MockErrorMessage, + }) + require.Equal(t, Mock{}, err) +} \ No newline at end of file diff --git a/samples/krill/lib/errors/fiber.go b/samples/krill/lib/errors/fiber.go new file mode 100644 index 0000000..2562335 --- /dev/null +++ b/samples/krill/lib/errors/fiber.go @@ -0,0 +1,90 @@ +package errors + +import ( + "errors" + "fmt" + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/iot-for-all/device-simulation/lib/logger" +) + +var FiberMappings = map[Category]int{ + BAD_REQUEST: http.StatusBadRequest, + NOT_FOUND: http.StatusNotFound, +} + +type FiberErrorHandler struct { + Logger logger.Logger +} + +func New(options ...func(*FiberErrorHandler)) *FiberErrorHandler { + handler := &FiberErrorHandler{ + Logger: &logger.NoopLogger{}, + } + + for _, option := range options { + option(handler) + } + + return handler +} + +func (handler *FiberErrorHandler) HandleError(c Context, err error) error { + code := fiber.StatusInternalServerError + + var e *fiber.Error + if errors.As(err, &e) { + code = e.Code + } + + internal, ok := err.(Error) + if ok { + mapping, ok := FiberMappings[internal.Code()] + if ok { + code = mapping + } + } + + if code >= fiber.StatusInternalServerError { + handler.Logger.Level(logger.Error).With("code", fmt.Sprintf("%d", code)).Printf("an internal error occurred") + } else { + handler.Logger.Level(logger.Debug).With("code", fmt.Sprintf("%d", code)).Printf("an non-500-level error occurred") + } + + err = c.Status(code).Send([]byte(err.Error())) + if err != nil { + handler.Logger.Level(logger.Error).With("error", err.Error()).Printf("error occurred when handling error") + return err + } + + return nil +} + +type Context interface { + Status(status int) Context + Send(body []byte) error +} + +type FiberContextWrapper struct { + *fiber.Ctx +} + +func (c *FiberContextWrapper) Status(status int) Context { + return &FiberContextWrapper{ + Ctx: c.Ctx.Status(status), + } +} + +type MockContext struct { + OnStatus func(status int) Context + OnSend func(body []byte) error +} + +func (ctx *MockContext) Status(status int) Context { + return ctx.OnStatus(status) +} + +func (ctx *MockContext) Send(body []byte) error { + return ctx.OnSend(body) +} diff --git a/samples/krill/lib/exporter/exporter.go b/samples/krill/lib/exporter/exporter.go new file mode 100644 index 0000000..041e78b --- /dev/null +++ b/samples/krill/lib/exporter/exporter.go @@ -0,0 +1,153 @@ +package exporter + +import ( + "encoding/json" + "fmt" + "io" + "io/fs" + "math" + "os" + "path/filepath" + "sync" + "time" + + "github.com/iot-for-all/device-simulation/components/registry" +) + +type Exporter interface { + RegisterHistogram(name, help string, start, width int) (Provider, error) +} + +type Provider interface { + Export() error + Label(label Label) registry.CancellableObservable +} + +type ( + Label string + Kind string +) + +const ( + HISTOGRAM Kind = "histogram" + COUNTER Kind = "counter" +) + +type HistogramOptions struct { + Start int `json:"start"` + Width int `json:"width"` +} + +type HistogramProvider struct { + file io.WriteCloser + mu sync.Mutex + Marshal func(v any) ([]byte, error) + + Name string `json:"name"` + Help string `json:"help"` + Kind Kind `json:"kind"` + Options HistogramOptions `json:"options"` + Data map[Label]map[int]int `json:"data"` +} + +func (provider *HistogramProvider) Export() error { + provider.mu.Lock() + defer provider.mu.Unlock() + + content, err := provider.Marshal(provider) + if err != nil { + return err + } + + _, err = provider.file.Write(content) + if err != nil { + return err + } + + return provider.file.Close() +} + +func (provider *HistogramProvider) Label(label Label) registry.CancellableObservable { + provider.mu.Lock() + defer provider.mu.Unlock() + histogram := &Histogram{ + Data: make(map[int]int), + options: provider.Options, + } + provider.Data[label] = histogram.Data + + return histogram +} + +type Histogram struct { + mu sync.Mutex + options HistogramOptions + Data map[int]int `json:"data"` +} + +func (histogram *Histogram) Observe(value float64) { + histogram.mu.Lock() + defer histogram.mu.Unlock() + + histogram.Data[(int(math.Floor(value))-histogram.options.Start)/histogram.options.Width]++ +} + +func (histogram *Histogram) Cancel() {} + +type Opener interface { + Open(filename string) (io.WriteCloser, error) +} + +type FileExporter struct { + opener Opener +} + +func NewExporter(opener Opener) *FileExporter { + return &FileExporter{ + opener: opener, + } +} + +func (exporter *FileExporter) RegisterHistogram(name, help string, start, width int) (Provider, error) { + + now := time.Now() + f, err := exporter.opener.Open(fmt.Sprintf("D%s-T%d-%d-%d-histogram-%s.json", now.Format(time.DateOnly), now.Hour(), now.Minute(), now.Second(), name)) + if err != nil { + return nil, err + } + + return &HistogramProvider{ + file: f, + Name: name, + Kind: HISTOGRAM, + Help: help, + Marshal: json.Marshal, + Options: HistogramOptions{ + Start: start, + Width: width, + }, + Data: make(map[Label]map[int]int), + }, nil +} + +type FileOpener struct { + storage string + OpenFile func(name string, flag int, perm fs.FileMode) (*os.File, error) +} + +func NewOpener(storage string, options ...func(*FileOpener)) *FileOpener { + opener := &FileOpener{ + storage: storage, + OpenFile: os.OpenFile, + } + + for _, option := range options { + option(opener) + } + + return opener +} + +func (fileOpener *FileOpener) Open(filename string) (io.WriteCloser, error) { + return fileOpener.OpenFile(filepath.Join(fileOpener.storage, filename), os.O_RDWR|os.O_CREATE, 0755) +} diff --git a/samples/krill/lib/exporter/exporter_test.go b/samples/krill/lib/exporter/exporter_test.go new file mode 100644 index 0000000..ca0e085 --- /dev/null +++ b/samples/krill/lib/exporter/exporter_test.go @@ -0,0 +1,236 @@ +package exporter + +import ( + "io" + "io/fs" + "os" + "testing" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/logger" + "github.com/stretchr/testify/require" +) + +const ( + MockLabel = "MockLabel" +) + +var ( + MockContent = []byte{0, 1} +) + +type MockError struct{} + +func (*MockError) Error() string { + return "mock" +} + +func TestMain(m *testing.M) { + m.Run() +} + +func TestHistogram(t *testing.T) { + exporter := NewExporter(&MockOpener{ + OnOpen: func(string) (io.WriteCloser, error) { + return nil, nil + }, + }) + + provider, err := exporter.RegisterHistogram("", "", 0, 10) + require.NoError(t, err) + + histProvider, ok := provider.(*HistogramProvider) + require.True(t, ok) + + histogram := provider.Label("") + + histogram.Observe(20) + + require.Equal(t, 1, histProvider.Data[""][2]) + + histogram.Observe(19) + + require.Equal(t, 1, histProvider.Data[""][1]) +} + +func TestHistogramNonZeroStart(t *testing.T) { + exporter := NewExporter(&MockOpener{ + OnOpen: func(string) (io.WriteCloser, error) { + return nil, nil + }, + }) + + provider, err := exporter.RegisterHistogram("", "", 10, 10) + require.NoError(t, err) + + histProvider, ok := provider.(*HistogramProvider) + require.True(t, ok) + + histogram := provider.Label("") + + histogram.Observe(20) + + require.Equal(t, 1, histProvider.Data[""][1]) + + histogram.Observe(19) + + require.Equal(t, 1, histProvider.Data[""][0]) +} + +func TestOpenerError(t *testing.T) { + exporter := NewExporter(&MockOpener{ + OnOpen: func(string) (io.WriteCloser, error) { + return nil, &MockError{} + }, + }) + _, err := exporter.RegisterHistogram("", "", 0, 0) + require.Equal(t, &MockError{}, err) +} + +func TestFileOpener(t *testing.T) { + opener := NewOpener("", func(fo *FileOpener) { + fo.OpenFile = func(name string, flag int, perm fs.FileMode) (*os.File, error) { + return nil, nil + } + }) + + _, err := opener.Open("") + require.NoError(t, err) +} + +func TestMockExporter(t *testing.T) { + exporter := &MockExporter{ + OnRegisterHistogram: func(name, help string, start, width int) (Provider, error) { + return nil, nil + }, + } + + _, err := exporter.RegisterHistogram("", "", 0, 0) + require.NoError(t, err) +} + +func TestHistogramProvider(t *testing.T) { + provider := &HistogramProvider{ + file: &MockFile{ + OnWrite: func(p []byte) (n int, err error) { + require.Equal(t, MockContent, p) + return 0, nil + }, OnClose: func() error { + return nil + }, + }, Marshal: func(v any) ([]byte, error) { + return MockContent, nil + }, + } + + require.NoError(t, provider.Export()) +} + +func TestHistogramProviderMarshalError(t *testing.T) { + provider := &HistogramProvider{ + file: &MockFile{ + OnWrite: func(p []byte) (n int, err error) { + return 0, nil + }, OnClose: func() error { + return nil + }, + }, Marshal: func(v any) ([]byte, error) { + return nil, &MockError{} + }, + } + + require.Equal(t, &MockError{}, provider.Export()) +} + +func TestHistogramProviderFileWriteError(t *testing.T) { + provider := &HistogramProvider{ + file: &MockFile{ + OnWrite: func(p []byte) (n int, err error) { + return 0, &MockError{} + }, OnClose: func() error { + return nil + }, + }, Marshal: func(v any) ([]byte, error) { + return nil, nil + }, + } + + require.Equal(t, &MockError{}, provider.Export()) +} + +func TestCustomHistogramProvider(t *testing.T) { + provider, err := New(&MockExporter{ + OnRegisterHistogram: func(name, help string, start, width int) (Provider, error) { + return &MockProvider{ + OnExport: func() error { + return nil + }, OnLabel: func(label Label) registry.CancellableObservable { + require.Equal(t, MockLabel, string(label)) + return nil + }, + }, nil + }, + }, func(chp *CustomHistogramProvider) { + chp.Logger = &logger.NoopLogger{} + }) + require.NoError(t, err) + + _, err = provider.With(MockLabel) + require.NoError(t, err) +} + +func TestCustomHistogramProviderCancelError(t *testing.T) { + provider, err := New(&MockExporter{ + OnRegisterHistogram: func(name, help string, start, width int) (Provider, error) { + return &MockProvider{ + OnExport: func() error { + return &MockError{} + }, OnLabel: func(label Label) registry.CancellableObservable { + return nil + }, + }, nil + }, + }, func(chp *CustomHistogramProvider) { + chp.Logger = &logger.MockLogger{ + OnLevel: func(i int) logger.Logger { + require.Equal(t, logger.Error, i) + return chp.Logger + }, OnWith: func(s1, s2 string) logger.Logger { + require.Equal(t, "error", s1) + require.Equal(t, (&MockError{}).Error(), s2) + return chp.Logger + }, OnPrintf: func(s string, i ...interface{}) { + require.Equal(t, "could not export data to file", s) + }, + } + }) + require.NoError(t, err) + + require.Equal(t, &MockError{}, provider.Cancel()) +} + +func TestCustomHistogramProviderRegistrationError(t *testing.T) { + _, err := New(&MockExporter{ + OnRegisterHistogram: func(name, help string, start, width int) (Provider, error) { + return nil, &MockError{} + }, + }) + require.Equal(t, &MockError{}, err) +} + +func TestStat(t *testing.T) { + err := Stat(".") + require.NoError(t, err) +} + +func TestStatInvalidVolumeError(t *testing.T) { + err := Stat("") + require.Equal(t, (&InvalidVolumePath{}).Error(), err.Error()) +} + +func TestStatError(t *testing.T) { + err := Stat("\u0000") + require.Error(t, err) + _, ok := err.(*InvalidVolumePath) + require.False(t, ok) +} diff --git a/samples/krill/lib/exporter/mock.go b/samples/krill/lib/exporter/mock.go new file mode 100644 index 0000000..c919f65 --- /dev/null +++ b/samples/krill/lib/exporter/mock.go @@ -0,0 +1,49 @@ +package exporter + +import ( + "io" + + "github.com/iot-for-all/device-simulation/components/registry" +) + +type MockExporter struct { + OnRegisterHistogram func(name, help string, start, width int) (Provider, error) +} + +func (exporter *MockExporter) RegisterHistogram(name, help string, start, width int) (Provider, error) { + return exporter.OnRegisterHistogram(name, help, start, width) +} + +type MockOpener struct { + OnOpen func(filename string) (io.WriteCloser, error) +} + +func (opener *MockOpener) Open(filename string) (io.WriteCloser, error) { + return opener.OnOpen(filename) +} + +type MockFile struct { + OnWrite func(p []byte) (n int, err error) + OnClose func() error +} + +func (file *MockFile) Write(p []byte) (n int, err error) { + return file.OnWrite(p) +} + +func (file *MockFile) Close() error { + return file.OnClose() +} + +type MockProvider struct { + OnExport func() error + OnLabel func(label Label) registry.CancellableObservable +} + +func (provider *MockProvider) Export() error { + return provider.OnExport() +} + +func (provider *MockProvider) Label(label Label) registry.CancellableObservable { + return provider.OnLabel(label) +} diff --git a/samples/krill/lib/exporter/provider.go b/samples/krill/lib/exporter/provider.go new file mode 100644 index 0000000..047aa86 --- /dev/null +++ b/samples/krill/lib/exporter/provider.go @@ -0,0 +1,50 @@ +package exporter + +import ( + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/logger" +) + +type CustomHistogramProvider struct { + provider Provider + Name string + Help string + Start int + Width int + Logger logger.Logger +} + +func New(exporter Exporter, options ...func(*CustomHistogramProvider)) (*CustomHistogramProvider, error) { + provider := &CustomHistogramProvider{ + Logger: &logger.NoopLogger{}, + } + + for _, option := range options { + option(provider) + } + + histProv, err := exporter.RegisterHistogram(provider.Name, provider.Help, provider.Start, provider.Width) + if err != nil { + return nil, err + } + + provider.provider = histProv + + return provider, nil +} + +func (provider *CustomHistogramProvider) Cancel() error { + err := provider.provider.Export() + if err != nil { + provider.Logger.Level(logger.Error).With("error", err.Error()).Printf("could not export data to file") + return err + } + + return nil +} + +func (provider *CustomHistogramProvider) With( + label string, +) (registry.CancellableObservable, error) { + return provider.provider.Label(Label(label)), nil +} \ No newline at end of file diff --git a/samples/krill/lib/exporter/stat.go b/samples/krill/lib/exporter/stat.go new file mode 100644 index 0000000..3508f10 --- /dev/null +++ b/samples/krill/lib/exporter/stat.go @@ -0,0 +1,29 @@ +package exporter + +import ( + "fmt" + "os" +) + + +type InvalidVolumePath struct { + path string +} + +func (err *InvalidVolumePath) Error() string { + return fmt.Sprintf("no storage directory exists at path %s, please check for misconfigurations", err.path) +} + +func Stat(path string) error { + _, err := os.Stat(path) + if os.IsNotExist(err) { + return &InvalidVolumePath{ + path: path, + } + } + if err != nil { + return err + } + + return nil +} \ No newline at end of file diff --git a/samples/krill/lib/expression/expression.go b/samples/krill/lib/expression/expression.go new file mode 100644 index 0000000..0f318cb --- /dev/null +++ b/samples/krill/lib/expression/expression.go @@ -0,0 +1,465 @@ +// Package expression provides functionality to evaluate simple mathematic expressions and functions. +package expression + +import ( + "errors" + "fmt" + "go/ast" + "go/token" + "math" + "math/rand" + "strconv" + "strings" + "time" +) + +// Evaluator is an interface whose implementation should be able to evaluate basic expressions given an evaluation context. +// From this context, a float64 should be evaluated, or an error if an evaluation error occurs. +type Evaluator interface { + Evaluate(map[string]any) (any, error) +} + +// Errors which may occur during expression evaluation. +var ( + ErrCannotEvaluateExpr = errors.New( + "could not evaluate expression", + ) + ErrCannotEvaluateLiteral = errors.New("could not evaluate literal") + ErrCallExpressionInvalid = errors.New( + "could not evaluate call expression name", + ) + ErrFunctionDoesNotExist = errors.New("function does not exist") + ErrCannotEvaluateBinaryOpToken = errors.New( + "could not evaluate binary op token", + ) + ErrCannotEvaluateUnaryOpToken = errors.New( + "could not evaluate unary op token", + ) + ErrIncorrectArgumentCount = errors.New( + "too many or too few arguments were supplied to the function", + ) + ErrInvalidFunctionArguments = errors.New( + "the supplied function arguments are the correct type but are invalid", + ) + ErrIdentNotFound = errors.New( + "the ident could not be found in the evaluation context", + ) + ErrUnaryExpressionMustBeNumeric = errors.New( + "unary expressions such as '-' can only be applied on numbers", + ) + ErrBinaryExpressionMustBeNumeric = errors.New( + "binary expressions such as '+' or '*' can only be applied on numbers of the same type (both ints or both floats)", + ) + ErrInvalidFunctionArgumentType = errors.New( + "the supplied function arguments are invalid", + ) + ErrInvalidSelector = errors.New( + "selector cannot be used on a non-map type", + ) +) + +// FunctionType is an enum representing the functions which can be evaluated by the evaluator. +type FunctionType string + +// Names of each function which can be evaluated by the evaluator. +const ( + SIN FunctionType = "sin" + COS FunctionType = "cos" + TAN FunctionType = "tan" + ASIN FunctionType = "asin" + ACOS FunctionType = "acos" + ATAN FunctionType = "atan" + RAND FunctionType = "rand" + STR FunctionType = "str" + CONCAT FunctionType = "concat" + RANDSTR FunctionType = "randstr" + NOW FunctionType = "now" + DELTA FunctionType = "delta" + TOINT FunctionType = "int" + TOFLOAT FunctionType = "float" + AFTER FunctionType = "after" + ABS FunctionType = "abs" + PI FunctionType = "pi" +) + +type OperandType int + +const ( + FLOAT OperandType = iota + STRING + DATETIME + INTEGER + VOID +) + +var FunctionTypeOperandTypeMapping = map[FunctionType][]OperandType{ + SIN: {FLOAT}, + COS: {FLOAT}, + TAN: {FLOAT}, + ASIN: {FLOAT}, + ACOS: {FLOAT}, + ATAN: {FLOAT}, + RAND: {INTEGER, INTEGER}, + STR: {FLOAT, INTEGER}, + CONCAT: {STRING, STRING}, + RANDSTR: {INTEGER}, + NOW: {VOID}, + DELTA: {DATETIME, DATETIME}, + AFTER: {DATETIME, INTEGER}, + TOINT: {FLOAT}, + TOFLOAT: {INTEGER}, + ABS: {FLOAT}, + PI: {VOID}, +} + +var OperandTypeValidationMapping = map[OperandType]func(any) bool{ + FLOAT: func(a any) bool { + _, b := a.(float64) + return b + }, + STRING: func(a any) bool { + _, b := a.(string) + return b + }, + DATETIME: func(a any) bool { + _, b := a.(time.Time) + return b + }, + INTEGER: func(a any) bool { + _, b := a.(int) + return b + }, +} + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +) + +var FunctionTypeFunctionCurryMappings = map[FunctionType]any{ + RAND: func(i0 int) any { + return func(i1 int) any { + return rand.Intn(i1-i0) + i0 // #nosec G404 + } + }, + SIN: func(f0 float64) any { return math.Sin(f0) }, + COS: func(f0 float64) any { return math.Cos(f0) }, + TAN: func(f0 float64) any { return math.Tan(f0) }, + ASIN: func(f0 float64) any { return math.Asin(f0) }, + ACOS: func(f0 float64) any { return math.Acos(f0) }, + ATAN: func(f0 float64) any { return math.Atan(f0) }, + STR: func(f0 float64) any { + return func(i0 int) any { + return fmt.Sprintf(fmt.Sprintf("%%0.%df", i0), f0) + } + }, + RANDSTR: func(i0 int) any { + + str := make([]byte, i0) + + randoms := rand.Perm(len(letterBytes) - 1) + + for idx := 0; idx < i0; idx++ { + str[idx] = letterBytes[randoms[idx]] + } + + return string(str) + + }, + TOINT: func(f0 float64) any { return int(f0) }, + CONCAT: func(s0 string) any { + return func(s1 string) any { + return fmt.Sprintf("%s%s", s0, s1) + } + }, + NOW: func() any { return time.Now() }, + DELTA: func(t0 time.Time) any { + return func(t1 time.Time) any { + if t0.After(t1) { + return int(t0.Sub(t1).Milliseconds()) + } else { + return int(t1.Sub(t0).Milliseconds()) + } + } + }, + TOFLOAT: func(i0 int) any { return float64(i0) }, + AFTER: func(t0 time.Time) any { + return func(i0 int) any { + return t0.Add(time.Duration(i0) * time.Millisecond) + } + }, + ABS: func(f0 float64) any { return math.Abs(f0) }, + PI: func() any { return math.Pi }, +} + +// FunctionValidationMapping describes a validation function which will be run before the functions in the mapping above are called. +// This assures that no panics will occur in the evaluation of the above functions, +// and also that NaN will not be returned. +var FunctionValidationMapping = map[FunctionType]func(...float64) bool{ + SIN: func(f ...float64) bool { return math.IsNaN(f[0]) || math.IsInf(f[0], 0) }, + COS: func(f ...float64) bool { return math.IsNaN(f[0]) || math.IsInf(f[0], 0) }, + TAN: func(f ...float64) bool { return math.IsNaN(f[0]) || math.IsInf(f[0], 0) }, + ASIN: func(f ...float64) bool { return f[0] < -1 || f[0] > 1 }, + ACOS: func(f ...float64) bool { return f[0] < -1 || f[0] > 1 }, + ATAN: func(f ...float64) bool { return true }, + RAND: func(f ...float64) bool { return f[0] > f[1] }, + STR: func(f ...float64) bool { + return math.IsNaN(f[0]) || math.IsInf(f[0], 0) || math.IsNaN(f[0]) || + math.IsInf(f[0], 0) || + f[1] > 10 + }, + RANDSTR: func(f ...float64) bool { + return math.IsNaN(f[0]) || math.IsInf(f[0], 0) || f[0] < 1 + }, +} + +// Expression is an implementation of evaluator. +// It holds the underlying golang AST expression and provides an evaluation function which can be applied on the given expression. +type Expression struct { + expr ast.Expr +} + +// New creates an Expression, given an ast expression. +func New(expr ast.Expr) *Expression { + return &Expression{ + expr: expr, + } +} + +// Evaluate will resolve the expression's ast expression to a float64 value, +// given an environment of key value pairs which can be used to resolve symbols in the expression. +// An error will be returned if the expression cannot evaluate for any reason. +func (expression *Expression) Evaluate(env map[string]any) (any, error) { + return evalExpr(env, expression.expr) +} + +func evalExpr(env map[string]any, n ast.Expr) (any, error) { + switch expr := n.(type) { + case *ast.BinaryExpr: + return evalBinaryExpr(env, expr) + case *ast.CallExpr: + return evalCallExpr(env, expr) + case *ast.UnaryExpr: + return evalUnaryExpr(env, expr) + case *ast.SelectorExpr: + return evalSelectorExpr(env, expr) + case *ast.Ident: + return evalIdent(env, expr) + case *ast.ParenExpr: + return evalExpr(env, expr.X) + case *ast.BasicLit: + return evalBasicLit(expr) + default: + return 0, ErrCannotEvaluateExpr + } +} + +func evalSelectorExpr(env map[string]any, expr *ast.SelectorExpr) (any, error) { + res, err := evalExpr(env, expr.X) + if err != nil { + return 0, err + } + + m, ok := res.(map[string]any) + if !ok { + return 0, ErrInvalidSelector + } + + return evalIdent(m, expr.Sel) +} + +func evalBasicLit(lit *ast.BasicLit) (any, error) { + switch lit.Kind { + case token.INT: + return strconv.Atoi(lit.Value) + case token.FLOAT: + return strconv.ParseFloat(lit.Value, 64) + case token.STRING: + return strings.Trim(lit.Value, "\""), nil + default: + return 0, ErrCannotEvaluateLiteral + } +} + +func evalIdent(env map[string]any, ident *ast.Ident) (any, error) { + res, ok := env[ident.Name] + if !ok { + return 0, ErrIdentNotFound + } + + return res, nil +} + +func evalUnaryExpr(env map[string]any, expr *ast.UnaryExpr) (any, error) { + res, err := evalExpr(env, expr.X) + if err != nil { + return 0, err + } + + switch expr.Op { + case token.SUB: + switch val := res.(type) { + case float64: + return -1 * val, nil + case int: + return -1 * val, nil + default: + return 0, ErrUnaryExpressionMustBeNumeric + } + default: + return 0, ErrCannotEvaluateUnaryOpToken + } +} + +func evalCallExpr(env map[string]any, expr *ast.CallExpr) (any, error) { + ident, ok := expr.Fun.(*ast.Ident) + if !ok { + return 0, ErrCallExpressionInvalid + } + + typeMappings, ok := FunctionTypeOperandTypeMapping[FunctionType(ident.Name)] + if !ok { + return 0, ErrFunctionDoesNotExist + } + + // If the function is a void function, evaluate immediately and return result. + if typeMappings[0] == VOID { + return FunctionTypeFunctionCurryMappings[FunctionType(ident.Name)].(func() any)(), nil + } + + if len(typeMappings) != len(expr.Args) { + return 0, ErrIncorrectArgumentCount + } + + args := make([]any, len(typeMappings)) + + for idx, arg := range expr.Args { + res, err := evalExpr(env, arg) + if err != nil { + return 0, err + } + args[idx] = res + } + + f := FunctionTypeFunctionCurryMappings[FunctionType(ident.Name)] + + for idx, opType := range typeMappings { + if OperandTypeValidationMapping[opType](args[idx]) { + switch opType { + case FLOAT: + f = f.(func(float64) any)(args[idx].(float64)) + case STRING: + f = f.(func(string) any)(args[idx].(string)) + case DATETIME: + f = f.(func(time.Time) any)(args[idx].(time.Time)) + case INTEGER: + f = f.(func(int) any)(args[idx].(int)) + } + } else { + return 0, ErrInvalidFunctionArgumentType + } + } + + return f, nil +} + +var BinaryExprFunctionMapping = map[OperandType]map[token.Token]any{ + FLOAT: { + token.ADD: func(f0, f1 float64) any { + return f0 + f1 + }, + token.SUB: func(f0, f1 float64) any { + return f0 - f1 + }, + token.MUL: func(f0, f1 float64) any { + return f0 * f1 + }, + token.QUO: func(f0, f1 float64) any { + if f1 == 0 { + return 0 + } + return f0 / f1 + }, + token.XOR: func(f0, f1 float64) any { + return math.Pow(f0, f1) + }, + }, INTEGER: { + token.ADD: func(i0, i1 int) any { + return i0 + i1 + }, + token.SUB: func(i0, i1 int) any { + return i0 - i1 + }, + token.MUL: func(i0, i1 int) any { + return i0 * i1 + }, + token.QUO: func(i0, i1 int) any { + if i1 == 0 { + return 0 + } + return i0 / i1 + }, + token.XOR: func(i0, i1 int) any { + res := i0 + for count := 1; count < i1; count++ { + res *= i0 + } + return res + }, + token.REM: func(i0, i1 int) any { + if i1 == 0 { + return 0 + } + return i0 % i1 + }, + }, +} + +func evalBinaryExpr(env map[string]any, expr *ast.BinaryExpr) (any, error) { + lhs, err := evalExpr(env, expr.X) + if err != nil { + return 0, err + } + + rhs, err := evalExpr(env, expr.Y) + if err != nil { + return 0, err + } + + switch lVal := lhs.(type) { + case float64: + rVal, ok := rhs.(float64) + if !ok { + return 0, ErrBinaryExpressionMustBeNumeric + } + + mappings, ok := BinaryExprFunctionMapping[FLOAT][expr.Op] + if !ok { + return 0, ErrCannotEvaluateBinaryOpToken + } + + return mappings.(func(float64, float64) any)(lVal, rVal), nil + + case int: + rVal, ok := rhs.(int) + if !ok { + return 0, ErrBinaryExpressionMustBeNumeric + } + + mappings, ok := BinaryExprFunctionMapping[INTEGER][expr.Op] + if !ok { + return 0, ErrCannotEvaluateBinaryOpToken + } + + return mappings.(func(int, int) any)(lVal, rVal), nil + default: + return 0, ErrBinaryExpressionMustBeNumeric + } +} + +type MockEvaluator struct { + OnEvaluate func(map[string]any) (any, error) +} + +func (evaluator *MockEvaluator) Evaluate(m map[string]any) (any, error) { + return evaluator.OnEvaluate(m) +} \ No newline at end of file diff --git a/samples/krill/lib/expression/expression_test.go b/samples/krill/lib/expression/expression_test.go new file mode 100644 index 0000000..f84d1d4 --- /dev/null +++ b/samples/krill/lib/expression/expression_test.go @@ -0,0 +1,200 @@ +package expression + +import ( + "go/parser" + "math" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestSimpleExpression(t *testing.T) { + psr, err := parser.ParseExpr("1") + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(nil) + require.NoError(t, err) + + require.Equal(t, 1, res) +} + +func TestSimpleBinaryExpression(t *testing.T) { + psr, err := parser.ParseExpr("1 + 1") + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(nil) + require.NoError(t, err) + + require.Equal(t, 2, res) +} + +func TestSimpleStringExpression(t *testing.T) { + psr, err := parser.ParseExpr(`"expected"`) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(nil) + require.NoError(t, err) + + require.Equal(t, "expected", res) +} + +func TestSimpleStringFunctionCallExpression(t *testing.T) { + psr, err := parser.ParseExpr(`concat("hello", concat(" ", "world"))`) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(nil) + require.NoError(t, err) + + require.Equal(t, "hello world", res) +} + +func TestStringConversionFunctionCallExpression(t *testing.T) { + psr, err := parser.ParseExpr(`concat("hello", concat(" ", str(100.0, 3)))`) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(nil) + require.NoError(t, err) + + require.Equal(t, "hello 100.000", res) +} + +func TestSelectorExpression(t *testing.T) { + psr, err := parser.ParseExpr(`x.y.z`) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(map[string]any{ + "x": map[string]any{ + "y": map[string]any{ + "z": 10, + }, + }, + }) + require.NoError(t, err) + + require.Equal(t, 10, res) +} + +func TestCallExpressionWithIdent(t *testing.T) { + psr, err := parser.ParseExpr(`sin(pi)`) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(map[string]any{ + "pi": math.Pi, + }) + require.NoError(t, err) + require.InDelta(t, 0.0, res, 0.00000001) +} + +func TestCallExpressionWithNestedIdent(t *testing.T) { + psr, err := parser.ParseExpr(`cos(x.y.z)`) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(map[string]any{ + "x": map[string]any{ + "y": map[string]any{ + "z": 0.0, + }, + }, + }) + require.NoError(t, err) + require.Equal(t, 1.0, res) +} + +func TestAllExpressions(t *testing.T) { + psr, err := parser.ParseExpr( + `(concat("100 =" ,concat(" ", str(-(-(cos(x.y.z - sin(2.0 * pi) - 1.0) + 99.0)), 0))))`, + ) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(map[string]any{ + "x": map[string]any{ + "y": map[string]any{ + "z": 1.0, + }, + }, + "pi": math.Pi, + }) + require.NoError(t, err) + require.Equal(t, "100 = 100", res) +} + +func TestRandomString(t *testing.T) { + psr, err := parser.ParseExpr(`randstr(10)`) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(nil) + require.NoError(t, err) + + val, ok := res.(string) + require.Equal(t, true, ok) + + require.Equal(t, 10, len(val)) +} + +func TestBasicTimeCallExpression(t *testing.T) { + psr, err := parser.ParseExpr(`now()`) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(nil) + require.NoError(t, err) + + _, ok := res.(time.Time) + require.Equal(t, true, ok) +} + +func TestTimeCallExpression(t *testing.T) { + psr, err := parser.ParseExpr(`delta(now(), t)`) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(map[string]any{ + "t": time.Now(), + }) + require.NoError(t, err) + + val, ok := res.(int) + require.Equal(t, true, ok) + require.LessOrEqual(t, 0, val) +} + +func TestIntPowerExpression(t *testing.T) { + psr, err := parser.ParseExpr(`2 ^ 3`) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(map[string]any{}) + require.NoError(t, err) + + val, ok := res.(int) + require.Equal(t, true, ok) + require.Equal(t, 8, val) +} + +func TestFloatPowerExpression(t *testing.T) { + psr, err := parser.ParseExpr(`2.0 ^ 3.0`) + require.NoError(t, err) + + expr := New(psr) + res, err := expr.Evaluate(map[string]any{}) + require.NoError(t, err) + + val, ok := res.(float64) + require.Equal(t, true, ok) + require.Equal(t, 8.0, val) +} \ No newline at end of file diff --git a/samples/krill/lib/flatten/flatten.go b/samples/krill/lib/flatten/flatten.go new file mode 100644 index 0000000..9cad9bb --- /dev/null +++ b/samples/krill/lib/flatten/flatten.go @@ -0,0 +1,103 @@ +package flatten + +import ( + "errors" + "fmt" + "time" +) + +var ( + ErrInvalidType = errors.New("this type cannot be flattened") +) + +type Flattener interface { + Flatten(parent string, entry any) ([]Field, error) +} + +type CSVFlattener struct { + MergeStrings func(string, string) string + MergeStringAndInt func(string, int) string + FormatDatetime func(time.Time) string + FormatFloat func(float64) string +} + +func New(options ...func(*CSVFlattener)) *CSVFlattener { + flattener := &CSVFlattener{ + MergeStrings: func(s1, s2 string) string { + return fmt.Sprintf("%s__%s", s1, s2) + }, MergeStringAndInt: func(s string, i int) string { + return fmt.Sprintf("%s__field_%d", s, i) + }, FormatDatetime: func(t time.Time) string { + return t.Format(time.UnixDate) + }, FormatFloat: func(f float64) string { + return fmt.Sprintf("%0.2f", f) + }, + } + + for _, option := range options { + option(flattener) + } + + return flattener +} + +type Field struct { + Key string + Value string +} + +func (flattener *CSVFlattener) Flatten(parent string, entry any) ([]Field, error) { + + var fields []Field + + switch e := entry.(type) { + case map[string]any: + for k, v := range e { + res, err := flattener.Flatten(flattener.MergeStrings(parent, k), v) + if err != nil { + return nil, err + } + fields = append(fields, res...) + } + case []any: + for idx, v := range e { + res, err := flattener.Flatten(flattener.MergeStringAndInt(parent, idx), v) + if err != nil { + return nil, err + } + fields = append(fields, res...) + } + case int: + return []Field{{ + Key: parent, + Value: fmt.Sprintf("%d", e), + }}, nil + case float64: + return []Field{{ + Key: parent, + Value: flattener.FormatFloat(e), + }}, nil + case string: + return []Field{{ + Key: parent, + Value: e, + }}, nil + case time.Time: + return []Field{{ + Key: parent, + Value: flattener.FormatDatetime(e), + }}, nil + default: + return nil, ErrInvalidType + } + + return fields, nil +} + +type MockFlattener struct { + OnFlatten func(parent string, entry any) ([]Field, error) +} + +func (flattener *MockFlattener) Flatten(parent string, entry any) ([]Field, error) { + return flattener.OnFlatten(parent, entry) +} diff --git a/samples/krill/lib/flatten/flatten_test.go b/samples/krill/lib/flatten/flatten_test.go new file mode 100644 index 0000000..890ffa9 --- /dev/null +++ b/samples/krill/lib/flatten/flatten_test.go @@ -0,0 +1,88 @@ +package flatten + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestBasicType(t *testing.T) { + flattener := New() + + fields, err := flattener.Flatten("key", 1) + require.NoError(t, err) + + require.Equal(t, 1, len(fields)) + require.Equal(t, Field{ + Key: "key", + Value: "1", + }, fields[0]) +} + +func TestBasicTypeArray(t *testing.T) { + flattener := New() + + fields, err := flattener.Flatten("key", []any{1, "2"}) + require.NoError(t, err) + + require.Equal(t, 2, len(fields)) + require.Equal(t, []Field{{ + Key: "key__field_0", + Value: "1", + }, { + Key: "key__field_1", + Value: "2", + }}, fields) +} + +func TestBasicTypeMap(t *testing.T) { + flattener := New() + + fields, err := flattener.Flatten("key", map[string]any{"field_0": "1", "field_1": 2.0}) + require.NoError(t, err) + + require.Equal(t, 2, len(fields)) + require.ElementsMatch(t, []Field{{ + Key: "key__field_0", + Value: "1", + }, { + Key: "key__field_1", + Value: "2.00", + }}, fields) +} + +func TestComplexTypeMap(t *testing.T) { + flattener := New() + + fields, err := flattener.Flatten("key", map[string]any{"field_0": "1", "field_1": []any{2, "3"}}) + require.NoError(t, err) + + require.Equal(t, 3, len(fields)) + require.ElementsMatch(t, []Field{{Key: "key__field_0", Value: "1"}, {Key: "key__field_1__field_0", Value: "2"}, {Key: "key__field_1__field_1", Value: "3"}}, fields) +} + +func TestDatetime(t *testing.T) { + flattener := New() + + ts := time.Now() + + fields, err := flattener.Flatten("key", ts) + require.NoError(t, err) + + require.Equal(t, 1, len(fields)) + require.Equal(t, []Field{{Key: "key", Value: flattener.FormatDatetime(ts)}}, fields) +} + +func TestInvalidType(t *testing.T) { + flattener := New() + + type invalid int + + _, err := flattener.Flatten("key", invalid(1)) + require.Equal(t, ErrInvalidType, err) +} diff --git a/samples/krill/lib/gauge/gauge.go b/samples/krill/lib/gauge/gauge.go new file mode 100644 index 0000000..90dd0e4 --- /dev/null +++ b/samples/krill/lib/gauge/gauge.go @@ -0,0 +1,129 @@ +package gauge + +import ( + "fmt" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/errors" + "github.com/prometheus/client_golang/prometheus" +) + +type Provider struct { + Vec *prometheus.GaugeVec + registry prometheus.Registerer + Name string + Help string + Label string +} + +const ( + GaugeLabelKey = "gauge" + SimulationGaugeDefaultName = "simulation_gauge" + SimulationGaugeDefaultHelp = "Simulation gauge" +) + +type InvalidPrometheusGaugeVecNameError struct { + errors.BadRequest + name string +} + +func (err *InvalidPrometheusGaugeVecNameError) Error() string { + return fmt.Sprintf( + "could not create the gauge provider with the name %s because the name has already been registered or is invalid", + err.name, + ) +} + +type InvalidPrometheusGaugeLabelError struct { + errors.BadRequest + name string + label string +} + +func (err *InvalidPrometheusGaugeLabelError) Error() string { + return fmt.Sprintf( + "could not create the prometheus gauge with label %s from gauge provider %s because the label has already been used or is invalid", + err.label, + err.name, + ) +} + +func New( + reg prometheus.Registerer, + options ...func(*Provider), +) (*Provider, error) { + provider := &Provider{ + registry: reg, + Label: GaugeLabelKey, + Name: SimulationGaugeDefaultName, + Help: SimulationGaugeDefaultHelp, + } + + for _, option := range options { + option(provider) + } + + provider.Vec = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: provider.Name, + Help: provider.Help, + }, + []string{provider.Label}, + ) + + err := provider.registry.Register(provider.Vec) + + if err != nil { + return nil, &InvalidPrometheusGaugeVecNameError{ + name: provider.Name, + } + } + + return provider, nil +} + +func (provider *Provider) Cancel() error { + provider.registry.Unregister(provider.Vec) + return nil +} + +func (provider *Provider) With( + label string, +) (registry.CancellableObservable, error) { + + counter, err := provider.Vec.GetMetricWith( + prometheus.Labels{provider.Label: label}, + ) + + if err != nil { + return nil, &InvalidPrometheusGaugeLabelError{ + name: provider.Name, + label: label, + } + } + + return NewGauge(provider.registry, counter), nil +} + +type Gauge struct { + observable prometheus.Gauge + registry prometheus.Registerer +} + +func NewGauge( + reg prometheus.Registerer, + observable prometheus.Gauge, +) *Gauge { + return &Gauge{ + registry: reg, + observable: observable, + } +} + +func (gauge Gauge) Cancel() { + gauge.registry.Unregister(gauge.observable) +} + +func (gauge Gauge) Observe(f float64) { + gauge.observable.Set(f) +} \ No newline at end of file diff --git a/samples/krill/lib/gauge/gauge_test.go b/samples/krill/lib/gauge/gauge_test.go new file mode 100644 index 0000000..18c4355 --- /dev/null +++ b/samples/krill/lib/gauge/gauge_test.go @@ -0,0 +1,104 @@ +package gauge + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +const ( + name = "name" + label = "label" + invalid = "{}|}][" + invalidUTF8 = "\xc3\x28" +) + +func TestSimpleGaugeProvider(t *testing.T) { + registry := prometheus.NewRegistry() + + gaugeProvider, err := New(registry) + + require.NoError(t, err) + + gaugeProvider.Cancel() +} + +func TestGaugeProviderWithInvalidName(t *testing.T) { + registry := prometheus.NewRegistry() + + _, err := New(registry, func(cp *Provider) { + cp.Name = invalid + }) + + require.Equal(t, &InvalidPrometheusGaugeVecNameError{ + name: invalid, + }, err) +} + +func TestGaugeProviderWithDuplicateNames(t *testing.T) { + + registry := prometheus.NewRegistry() + + gaugeProvider, err := New(registry, func(cp *Provider) { + cp.Name = name + }) + + require.NoError(t, err) + + _, err = gaugeProvider.With(label) + + require.NoError(t, err) + + _, err = New(registry, func(cp *Provider) { + cp.Name = name + }) + + require.Equal(t, (&InvalidPrometheusGaugeVecNameError{ + name: name, + }).Error(), err.Error()) +} + +func TestGaugeWithLabel(t *testing.T) { + + registry := prometheus.NewRegistry() + + gaugeProvider, err := New(registry, func(cp *Provider) { + cp.Name = name + }) + + require.NoError(t, err) + + gauge, err := gaugeProvider.With(name) + + gauge.Observe(0) + gauge.Cancel() + + require.NoError(t, err) +} + +func TestGaugeWithLabelError(t *testing.T) { + + registry := prometheus.NewRegistry() + + gaugeProvider, err := New(registry, func(cp *Provider) { + cp.Name = name + }) + + require.NoError(t, err) + + _, err = gaugeProvider.With("") + + require.NoError(t, err) + + _, err = gaugeProvider.With(invalidUTF8) + + require.Equal(t, (&InvalidPrometheusGaugeLabelError{ + name: name, + label: invalidUTF8, + }).Error(), err.Error()) +} \ No newline at end of file diff --git a/samples/krill/lib/histogram/histogram.go b/samples/krill/lib/histogram/histogram.go new file mode 100644 index 0000000..8ce7eb6 --- /dev/null +++ b/samples/krill/lib/histogram/histogram.go @@ -0,0 +1,170 @@ +// Package histogram provides the histogram type implementation of the Provider interface. +// Histogram Providers represent the histogramVec prometheus metrics. +package histogram + +import ( + "fmt" + + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/lib/errors" + + "github.com/prometheus/client_golang/prometheus" +) + +// Provider is a registry.Provider implementation. +// Its purpose is to register a new prometheus histogramVec when created, +// And to create a histogram with a particular label when its with function is called. +type Provider struct { + HistogramVec *prometheus.HistogramVec + Name string + Help string + Label string + Start float64 + Width float64 + Buckets int + registry prometheus.Registerer +} + +const ( + SimulationHistogramDefaultName = "simulation_histogram" + SimulationHistogramDefaultHelp = "Simulation histogram" + SimulationHistogramDefaultBucketStart = 0 + SimulationHistogramDefaultBucketWidth = 1 + SimulationHistogramDefaultBucketCount = 1 + HistogramLabelKey = "histogram" +) + +type InvalidHistogramParametersError struct { + errors.BadRequest + buckets int + name string +} + +func (err *InvalidHistogramParametersError) Error() string { + return fmt.Sprintf( + "histogram provider with name %s had %d buckets specified, and there must be at least one bucket in a prometheus histogram", + err.name, + err.buckets, + ) +} + +type InvalidPrometheusHistogramVecNameError struct { + errors.BadRequest + name string +} + +func (err *InvalidPrometheusHistogramVecNameError) Error() string { + return fmt.Sprintf( + "could not create the histogram provider with the name %s because the name has already been registered or is invalid", + err.name, + ) +} + +type InvalidPrometheusHistogramLabelError struct { + errors.BadRequest + name string + label string +} + +func (err *InvalidPrometheusHistogramLabelError) Error() string { + return fmt.Sprintf( + "could not create the prometheus histogram with label %s from histogram provider %s because the label has already been used or is invalid", + err.label, + err.name, + ) +} + +// New creates a Provider, given a prometheus registerer. +// It can also take a function to set optional parameters. +func New( + reg prometheus.Registerer, + options ...func(*Provider), +) (*Provider, error) { + histogramProvider := &Provider{ + Name: SimulationHistogramDefaultName, + Help: SimulationHistogramDefaultHelp, + Start: SimulationHistogramDefaultBucketStart, + Width: SimulationHistogramDefaultBucketWidth, + Buckets: SimulationHistogramDefaultBucketCount, + Label: HistogramLabelKey, + registry: reg, + } + + for _, option := range options { + option(histogramProvider) + } + + if histogramProvider.Buckets <= 0 { + return nil, &InvalidHistogramParametersError{ + buckets: histogramProvider.Buckets, + name: histogramProvider.Name, + } + } + + histogramProvider.HistogramVec = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: histogramProvider.Name, + Help: histogramProvider.Help, + Buckets: prometheus.LinearBuckets( + histogramProvider.Start, + histogramProvider.Width, + histogramProvider.Buckets, + ), + }, + []string{histogramProvider.Label}, + ) + + err := reg.Register(histogramProvider.HistogramVec) + + if err != nil { + return nil, &InvalidPrometheusHistogramVecNameError{ + name: histogramProvider.Name, + } + } + + return histogramProvider, nil +} + +// Cancel unregisters the prometheus histogramVec from the prometheus registerer. +func (histogramProvider *Provider) Cancel() error { + histogramProvider.registry.Unregister(histogramProvider.HistogramVec) + return nil +} + +// With attempts to create a Histogram, which is a wrapper around the prometheus histogram metric that implements the CancellableObservable interface. +func (histogramProvider *Provider) With( + label string, +) (registry.CancellableObservable, error) { + histogram, err := histogramProvider.HistogramVec.GetMetricWith( + prometheus.Labels{histogramProvider.Label: label}, + ) + if err != nil { + return nil, &InvalidPrometheusHistogramLabelError{ + name: histogramProvider.Name, + label: label, + } + } + + return NewHistogram(histogram), nil +} + +// Histogram is an implementation of the CancellableObservable which wraps the functionality of the prometheus histogram metric. +// It observes a value into the prometheus histogram when its observe function is called. +type Histogram struct { + observable prometheus.Observer +} + +// NewHistogram creates a histogram given a prometheus histogram (in the form of an observable interface). +func NewHistogram(observable prometheus.Observer) *Histogram { + return &Histogram{ + observable: observable, + } +} + +// Observer calls the prometheus histograms observe function to observe a new value into the histogram. +func (histogram Histogram) Observe(value float64) { + histogram.observable.Observe(value) +} + +// Cancel is a no-op because a prometheus observable cannot be unregistered. +func (histogram Histogram) Cancel() {} \ No newline at end of file diff --git a/samples/krill/lib/histogram/histogram_test.go b/samples/krill/lib/histogram/histogram_test.go new file mode 100644 index 0000000..2a223fd --- /dev/null +++ b/samples/krill/lib/histogram/histogram_test.go @@ -0,0 +1,110 @@ +package histogram + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +const ( + name = "name" + label = "label" + invalid = "{}|}][" + invalidUTF8 = "\xc3\x28" +) + +func TestSimpleHistogramProvider(t *testing.T) { + registry := prometheus.NewRegistry() + + histogramProvider, err := New(registry) + + require.NoError(t, err) + + histogramProvider.Cancel() +} + +func TestHistogramProviderWithLabel(t *testing.T) { + registry := prometheus.NewRegistry() + + histogramProvider, err := New(registry, func(hp *Provider) { + hp.Name = name + }) + + require.NoError(t, err) + + histogram, err := histogramProvider.With(label) + + histogram.Observe(0) + histogram.Cancel() + + require.NoError(t, err) +} + +func TestHistogramProviderWithInvalidName(t *testing.T) { + registry := prometheus.NewRegistry() + + _, err := New(registry, func(hp *Provider) { + hp.Name = invalid + }) + + require.Equal(t, InvalidPrometheusHistogramVecNameError{ + name: invalid, + }, *err.(*InvalidPrometheusHistogramVecNameError)) +} + +func TestHistogramProviderWithInvalidBuckets(t *testing.T) { + registry := prometheus.NewRegistry() + buckets := -1 + + _, err := New(registry, func(hp *Provider) { + hp.Buckets = buckets + hp.Name = name + + }) + + require.Equal(t, &InvalidHistogramParametersError{ + buckets: buckets, + name: name, + }, err) +} + +func TestHistogramWithDuplicateNames(t *testing.T) { + registry := prometheus.NewRegistry() + + _, err := New(registry, func(hp *Provider) { + hp.Name = name + }) + + require.NoError(t, err) + + _, err = New(registry, func(hp *Provider) { + hp.Name = name + }) + + require.Equal(t, &InvalidPrometheusHistogramVecNameError{ + name: name, + }, err) +} + +func TestHistogramWithInvalidLabelName(t *testing.T) { + + registry := prometheus.NewRegistry() + + histogramProvider, err := New(registry, func(hp *Provider) { + hp.Name = name + }) + + require.NoError(t, err) + + _, err = histogramProvider.With(invalidUTF8) + + require.Equal(t, &InvalidPrometheusHistogramLabelError{ + name: name, + label: invalidUTF8, + }, err) +} \ No newline at end of file diff --git a/samples/krill/lib/krill/configuration.go b/samples/krill/lib/krill/configuration.go new file mode 100644 index 0000000..1a77867 --- /dev/null +++ b/samples/krill/lib/krill/configuration.go @@ -0,0 +1,138 @@ +package krill + +type ( + // Tag describes the structure of a simulated tag, including its ID, + // count (of how many instances of the tag to create), configuration (the equation which describes the behavior of this tag), + // and the missingChance (the percent chance that the tag will not be rendered). + Tag struct { + ID string `json:"id" yaml:"id"` + Configuration string `json:"configuration" yaml:"configuration"` + Count int `json:"count" yaml:"count"` + MissingChance int `json:"missingChance" yaml:"missingChance"` + } + + // Rate describes the flow of message publishes from the simulator, + // where limit describes the maximum number of services which leave the simulator over a period of PeriodSeconds. + // TagsPerMessage describes how many tags should be rendered per message sent. + Rate struct { + MessagesPerPeriod int `json:"messagesPerPeriod" yaml:"messagesPerPeriod"` + PeriodSeconds int `json:"periodSeconds" yaml:"periodSeconds"` + TagsPerMessage int `json:"tagsPerMessage" yaml:"tagsPerMessage"` + } + + // Site describes a logical collection of simulated devices. + Site struct { + // Name is the name of a site which will be used in creating topics for assets in this site. + Name string `json:"name" yaml:"name"` + + // Tags is a collection of tags which each asset in this site will send. + Tags []Tag `json:"tags" yaml:"tags"` + + // AssetCount is the number of total assets in this site, each represented by their own MQTT connection to the broker. + AssetCount int `json:"assetCount" yaml:"assetCount"` + + // Rate describes the flow of messages for each asset. + Rate Rate `json:"rate" yaml:"rate"` + + // PayloadFormat describes the shape of data sent by each asset (JSON, binary, CSV, etc.). + PayloadFormat string `json:"payloadFormat" yaml:"payloadFormat"` + + // TopicFormat describes the format of the template that each asset will publish messages on. + TopicFormat string `json:"topicFormat" yaml:"topicFormat"` + + // QoSLevel describes the QoS level of all message publishes by all assets. + QoSLevel int `json:"qosLevel" yaml:"qosLevel"` + + // MQTTVersion describes the MQTT protocol version to use for this site (v3 or v5). + MQTTVersion string `json:"mqttVersion" yaml:"mqttVersion"` + } + + // Target describes the target MQTT broker host and port. + Target struct { + Host string `json:"host" yaml:"host"` + Port int `json:"port" yaml:"port"` + } + + // Simulation describes the overall configuration for a simulation, including + // a collection of sites, a collection of refDatas, and an MQTT target. + Simulation struct { + Sites []Site `json:"sites" yaml:"sites"` + Target Target `json:"target" yaml:"target"` + } + + // Ports describes the ports of prometheus metrics data and the ref data application server. + Ports struct { + Metrics int `json:"metrics" yaml:"metrics"` + RefData int `json:"refData" yaml:"refData"` + } + + // Configuration describes the overall configuration structure for the krill simulator. + Configuration struct { + Simulation Simulation `json:"simulation" yaml:"simulation"` + Ports Ports `json:"ports" yaml:"ports"` + LogLevel int `json:"logLevel" yaml:"logLevel"` + } + + TopicTemplate struct { + SiteName string + AssetName string + TagName string + } +) + +const ( + BrokerID = "0" + ClientIDFormat = "%s__asset_%d" + TopicIDFormat = "%s__%s__%s" + TagIDFormat = "%s__%s__%d" + TagParentIDFormat = "%s__parent" + TagChildIDFormat = "%s__child" + ProviderIDFormat = "krill_%s_asset_publish_counter" + + TagTimestampIDFormat = "%s__timestamp" + OPCUATimeExpression = "now()" + OPCUATimeConfiguration = "Timestamp" + + TagSequenceIDFormat = "%s__sequence" + OPCUASequenceExpression = "x" + OPCUASequenceConfiguration = "SequenceNumber" + + TagDatasetWriterIDFormat = "%s__dataset_writer" + OPCUADatasetWriterExpression = `concat(site, concat("_", id))` + OPCUADatasetWriterConfiguration = "DataSetWriterName" + + TagPayloadIDFormat = "%s__payload" + OPCUAPayloadConfiguration = "Payload" + + TagValueIDFormat = "%s__value" + OPCUAValueConfiguration = "Value" + + TagSourceTimestampIDFormat = "%s__source_timestamp" + OPCUASourceTimeExpression = OPCUATimeExpression + OPCUASourceTimestampConfiguration = "SourceTimestamp" +) + +const ( + Krill = ` +⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣤⣤⣀⠀⠀⠀⠀⠀⠀ +⠀⠀⢀⣀⡙⠻⢶⣶⣦⣴⣶⣶⣶⠾⠛⠛⠋⠉⠉⠉⠉⠙⠃⠀⠀⠀⠀⠀ +⠀⠀⠀⠉⠉⠙⠛⠛⠋⠉⠉⠡⣤⣴⣶⣶⣾⣿⣿⣿⣛⣩⣤⡤⠖⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⢠⣴⣾⠂⣴⣦⠈⣿⣿⣿⣿⣿⣿⠿⠛⣋⠁⠀⠀⠀⠀⠀ +⠀⠀⢀⣼⣿⣶⣄⡉⠻⣧⣌⣁⣴⣿⣿⣿⣿⣿⣿⡿⠛⠁⠀⠀⠀⠀⠀⠀ +⠀⠀⣾⣿⣿⣿⣿⣿⣦⡈⢻⣿⣿⣿⣿⡿⠿⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⡀⢻⣿⣿⣿⣿⣿⣿⣿⡄⠙⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⢠⣷⣄⡉⠻⢿⣿⣿⣿⠏⠠⢶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⢸⣿⣿⣿⣶⣤⣈⠙⠁⠰⣦⣀⠉⠻⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠘⢿⣿⣿⣿⣿⣿⡇⠠⣦⣄⠉⠳⣤⠈⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⢠⣌⣉⡉⠉⣉⡁⠀⠀⠙⠗⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠹⢿⣿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠙⠻⣿⣿⠟⢀⣤⡀⠀⠀⠀⠀⠀⠀⣀⣀⣠⣤⣤⣤⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠿⠿⡿⠂⣀⣠⣤⣤⣤⣀⣉⣉⠉⠉⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠙⠛⠛⠛⠛⠋⠉⠉⠁⠀⠀⠀⠀ + _ _ _ _ + | |__ _ _ <_>| || | + | / /| '_>| || || | + |_\_\|_| |_||_||_| + +` +) diff --git a/samples/krill/lib/krill/krill.go b/samples/krill/lib/krill/krill.go new file mode 100644 index 0000000..c7d4009 --- /dev/null +++ b/samples/krill/lib/krill/krill.go @@ -0,0 +1,470 @@ +package krill + +import ( + "errors" + "fmt" + "io" + + "github.com/iot-for-all/device-simulation/components/broker" + "github.com/iot-for-all/device-simulation/components/client" + "github.com/iot-for-all/device-simulation/components/edge" + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/components/limiter" + "github.com/iot-for-all/device-simulation/components/node" + "github.com/iot-for-all/device-simulation/components/observer" + "github.com/iot-for-all/device-simulation/components/outlet" + "github.com/iot-for-all/device-simulation/components/provider" + "github.com/iot-for-all/device-simulation/components/publisher" + "github.com/iot-for-all/device-simulation/components/registry" + "github.com/iot-for-all/device-simulation/components/renderer" + "github.com/iot-for-all/device-simulation/components/site" + "github.com/iot-for-all/device-simulation/components/subscriber" + "github.com/iot-for-all/device-simulation/components/topic" + "github.com/iot-for-all/device-simulation/components/tracer" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/iot-for-all/device-simulation/lib/templater" +) + +type KrillBuilder struct { + broker component.Service[*broker.Component, component.ID] + client component.Service[*client.Component, component.ID] + edge component.Service[*edge.Component, component.ID] + formatter component.Service[*formatter.Component, component.ID] + limiter component.Service[*limiter.Component, component.ID] + node component.Service[*node.Component, component.ID] + observer component.Service[*observer.Component, component.ID] + outlet component.Service[*outlet.Component, component.ID] + provider component.Service[*provider.Component, component.ID] + publisher component.Service[*publisher.Component, component.ID] + registry component.Service[*registry.Component, component.ID] + renderer component.Service[*renderer.Component, component.ID] + site component.Service[*site.Component, component.ID] + subscriber component.Service[*subscriber.Component, component.ID] + topic component.Service[*topic.Component, component.ID] + tracer component.Service[*tracer.Component, component.ID] +} + +func New( + broker component.Service[*broker.Component, component.ID], + client component.Service[*client.Component, component.ID], + edge component.Service[*edge.Component, component.ID], + formatter component.Service[*formatter.Component, component.ID], + limiter component.Service[*limiter.Component, component.ID], + node component.Service[*node.Component, component.ID], + observer component.Service[*observer.Component, component.ID], + outlet component.Service[*outlet.Component, component.ID], + provider component.Service[*provider.Component, component.ID], + publisher component.Service[*publisher.Component, component.ID], + registry component.Service[*registry.Component, component.ID], + renderer component.Service[*renderer.Component, component.ID], + site component.Service[*site.Component, component.ID], + subscriber component.Service[*subscriber.Component, component.ID], + topic component.Service[*topic.Component, component.ID], + tracer component.Service[*tracer.Component, component.ID], +) *KrillBuilder { + return &KrillBuilder{ + broker: broker, + client: client, + edge: edge, + formatter: formatter, + limiter: limiter, + node: node, + observer: observer, + outlet: outlet, + provider: provider, + publisher: publisher, + registry: registry, + renderer: renderer, + site: site, + subscriber: subscriber, + topic: topic, + tracer: tracer, + } +} + +func (builder *KrillBuilder) Parse(configuration Simulation) error { + err := builder.broker.Create(BrokerID, &broker.Component{ + Broker: configuration.Target.Host, + Port: configuration.Target.Port, + }) + if err != nil { + return err + } + + for _, site := range configuration.Sites { + err := builder.ParseSite(site) + if err != nil { + return err + } + } + + return nil +} + +func (builder *KrillBuilder) ParseSite(configuration Site) error { + err := builder.site.Create(component.ID(configuration.Name), &site.Component{ + Name: configuration.Name, + }) + if err != nil { + return err + } + + tags, err := builder.ParseTags(configuration) + if err != nil { + return err + } + + err = builder.provider.Create(component.ID(configuration.Name), &provider.Component{ + Type: provider.COUNTER, + Name: fmt.Sprintf(ProviderIDFormat, configuration.Name), + }) + if err != nil { + return err + } + + assets, err := builder.ParseAssets(configuration) + if err != nil { + return err + } + + err = builder.ParseTopics(configuration, tags, assets) + if err != nil { + return err + } + + return nil +} + +func (builder *KrillBuilder) ParseTopics(configuration Site, tags []string, assets []string) error { + templateExecutor, err := templater.NewExecutor(configuration.TopicFormat) + if err != nil { + return err + } + + topicTemplate := templater.New[TopicTemplate](templateExecutor) + + for _, asset := range assets { + for _, tag := range tags { + topicID := fmt.Sprintf(TopicIDFormat, configuration.Name, asset, tag) + + reader, err := topicTemplate.Render(TopicTemplate{ + SiteName: configuration.Name, + AssetName: asset, + TagName: tag, + }) + if err != nil { + return err + } + + res, err := io.ReadAll(reader) + if err != nil { + return err + } + + err = builder.ParseTopicAndPublisher(configuration, topicID, string(res), asset, tag) + if err != nil { + return err + } + } + } + + return nil +} + +func (builder *KrillBuilder) ParseTopicAndPublisher(configuration Site, id string, name string, asset string, tag string) error { + err := builder.topic.Create(component.ID(id), &topic.Component{ + Name: name, + }) + if err != nil { + return err + } + + err = builder.limiter.Create(component.ID(id), &limiter.Component{ + Limit: configuration.Rate.MessagesPerPeriod, + PeriodSeconds: configuration.Rate.PeriodSeconds, + }) + if err != nil { + return err + } + + return builder.publisher.Create(component.ID(id), &publisher.Component{ + TopicID: component.ID(id), + ClientID: component.ID(asset), + RendererID: component.ID(tag), + QoSLevel: configuration.QoSLevel, + LimiterID: component.ID(id), + RendersPerPublish: configuration.Rate.TagsPerMessage, + }) +} + +func (builder *KrillBuilder) ParseTags(configuration Site) ([]string, error) { + switch configuration.PayloadFormat { + case "JSONTagPerMessage": + return builder.ParseJSONTagPerMessage(configuration) + case "JSON": + return builder.ParseComplex(configuration, formatter.JSON) + case "OPCUA": + return builder.ParseOPCUA(configuration) + case "BigEndian": + return builder.ParseFlat(configuration, "bigEndian") + case "LittleEndian": + return builder.ParseFlat(configuration, "littleEndian") + case "CSV": + return builder.ParseComplex(configuration, formatter.CSV) + case "Protobuf": + return builder.ParseComplex(configuration, formatter.PROTOBUF) + default: + return nil, errors.New("invalid payload format") + } +} + +func (builder *KrillBuilder) ParseAssets(configuration Site) ([]string, error) { + assets := make([]string, configuration.AssetCount) + for count := 0; count < configuration.AssetCount; count++ { + asset, err := builder.ParseAsset(configuration, count) + if err != nil { + return nil, err + } + assets[count] = asset + } + + return assets, nil +} + +func (builder *KrillBuilder) ParseAsset(configuration Site, count int) (string, error) { + clientID := fmt.Sprintf(ClientIDFormat, configuration.Name, count) + + err := builder.registry.Create(component.ID(clientID), ®istry.Component{}) + if err != nil { + return "", err + } + + err = builder.observer.Create(component.ID(clientID), &observer.Component{ + RegistryID: component.ID(clientID), + ProviderID: component.ID(configuration.Name), + Label: clientID, + }) + if err != nil { + return "", err + } + + err = builder.client.Create(component.ID(clientID), &client.Component{ + SiteID: component.ID(configuration.Name), + BrokerID: BrokerID, + Name: clientID, + Type: client.Type(configuration.MQTTVersion), + RegistryID: component.ID(clientID), + }) + if err != nil { + return "", err + } + + return clientID, nil +} + +func (builder *KrillBuilder) ParseJSONTagPerMessage(configuration Site) ([]string, error) { + err := builder.formatter.Create(component.ID(configuration.Name), &formatter.Component{ + Type: formatter.JSON, + }) + if err != nil { + return nil, err + } + + var tagNames []string + for _, tag := range configuration.Tags { + for count := 0; count < tag.Count; count++ { + tagName, err := builder.ParseJSONTag(configuration.Name, tag, count) + if err != nil { + return nil, err + } + tagNames = append(tagNames, tagName) + } + } + + return tagNames, nil +} + +func (builder *KrillBuilder) ParseJSONTag(siteName string, tag Tag, count int) (string, error) { + tagID := fmt.Sprintf(TagIDFormat, siteName, tag.ID, count) + childID := fmt.Sprintf(TagChildIDFormat, tagID) + + err := builder.ParseRootNode(siteName, tagID, node.COLLECTION) + if err != nil { + return "", err + } + + err = builder.ParseExpressionNode(tagID, childID, tagID, tag.Configuration, tagID, edge.LABEL) + if err != nil { + return "", err + } + + return tagID, nil +} + +func (builder *KrillBuilder) ParseOPCUA(configuration Site) ([]string, error) { + err := builder.ParseFormatter(configuration.Name, formatter.JSON, node.COLLECTION) + if err != nil { + return nil, err + } + + timestampID := fmt.Sprintf(TagTimestampIDFormat, configuration.Name) + + err = builder.ParseExpressionNode(configuration.Name, timestampID, timestampID, OPCUATimeExpression, OPCUATimeConfiguration, edge.LABEL) + if err != nil { + return nil, err + } + + sequenceID := fmt.Sprintf(TagSequenceIDFormat, configuration.Name) + + err = builder.ParseExpressionNode(configuration.Name, sequenceID, sequenceID, OPCUASequenceExpression, OPCUASequenceConfiguration, edge.LABEL) + if err != nil { + return nil, err + } + + datasetWriterID := fmt.Sprintf(TagDatasetWriterIDFormat, configuration.Name) + + err = builder.ParseExpressionNode(configuration.Name, datasetWriterID, datasetWriterID, OPCUADatasetWriterExpression, OPCUADatasetWriterConfiguration, edge.LABEL) + if err != nil { + return nil, err + } + + payloadID := fmt.Sprintf(TagPayloadIDFormat, configuration.Name) + + err = builder.ParseCollectionNode(configuration.Name, payloadID, payloadID, OPCUAPayloadConfiguration) + if err != nil { + return nil, err + } + + for _, tag := range configuration.Tags { + for count := 0; count < tag.Count; count++ { + err := builder.ParseOPCUATag(configuration, payloadID, tag, count) + if err != nil { + return nil, err + } + } + } + + return []string{configuration.Name}, nil +} + +func (builder *KrillBuilder) ParseOPCUATag(configuration Site, rootId string, tag Tag, count int) error { + tagID := fmt.Sprintf(TagIDFormat, configuration.Name, tag.ID, count) + + err := builder.ParseCollectionNode(rootId, tagID, tagID, tagID) + if err != nil { + return err + } + + valueID := fmt.Sprintf(TagValueIDFormat, tagID) + + err = builder.ParseExpressionNode(tagID, valueID, valueID, tag.Configuration, OPCUAValueConfiguration, edge.LABEL) + if err != nil { + return err + } + + sourceTimestampID := fmt.Sprintf(TagSourceTimestampIDFormat, tagID) + + return builder.ParseExpressionNode(tagID, sourceTimestampID, sourceTimestampID, OPCUASourceTimeExpression, OPCUASourceTimestampConfiguration, edge.LABEL) +} + +func (builder *KrillBuilder) ParseExpressionNode(rootNodeId, nodeId, edgeId, nodeExpression string, edgeConfiguration any, edgeType edge.Type) error { + err := builder.node.Create(component.ID(nodeId), &node.Component{ + Type: node.EXPRESSION, + Configuration: nodeExpression, + }) + if err != nil { + return err + } + + return builder.edge.Create(component.ID(edgeId), &edge.Component{ + ParentNodeId: component.ID(rootNodeId), + ChildNodeId: component.ID(nodeId), + Type: edgeType, + Configuration: edgeConfiguration, + }) +} + +func (builder *KrillBuilder) ParseCollectionNode(rootNodeId, nodeId, edgeId, edgeConfiguration string) error { + err := builder.node.Create(component.ID(nodeId), &node.Component{ + Type: node.COLLECTION, + }) + if err != nil { + return err + } + + return builder.edge.Create(component.ID(edgeId), &edge.Component{ + ParentNodeId: component.ID(rootNodeId), + ChildNodeId: component.ID(nodeId), + Type: edge.LABEL, + Configuration: edgeConfiguration, + }) +} + +func (builder *KrillBuilder) ParseComplex(configuration Site, format formatter.Type) ([]string, error) { + err := builder.ParseFormatter(configuration.Name, format, node.COLLECTION) + if err != nil { + return nil, err + } + + for _, tag := range configuration.Tags { + for count := 0; count < tag.Count; count++ { + tagID := fmt.Sprintf(TagIDFormat, configuration.Name, tag.ID, count) + + err := builder.ParseExpressionNode(configuration.Name, tagID, tagID, tag.Configuration, tagID, edge.LABEL) + if err != nil { + return nil, err + } + } + } + + return []string{configuration.Name}, nil +} + +func (builder *KrillBuilder) ParseFormatter(id string, format formatter.Type, nodeType node.Type) error { + err := builder.formatter.Create(component.ID(id), &formatter.Component{ + Type: format, + }) + if err != nil { + return err + } + + return builder.ParseRootNode(id, id, nodeType) +} + +func (builder *KrillBuilder) ParseRootNode(formatterID string, id string, nodeType node.Type) error { + err := builder.node.Create(component.ID(id), &node.Component{ + Type: nodeType, + }) + if err != nil { + return err + } + + return builder.renderer.Create(component.ID(id), &renderer.Component{ + FormatterID: component.ID(formatterID), + NodeID: component.ID(id), + }) +} + +func (builder *KrillBuilder) ParseFlat(configuration Site, format formatter.Type) ([]string, error) { + + err := builder.ParseFormatter(configuration.Name, format, node.ARRAY) + if err != nil { + return nil, err + } + + field := 0 + for _, tag := range configuration.Tags { + for count := 0; count < tag.Count; count++ { + tagID := fmt.Sprintf(TagIDFormat, configuration.Name, tag.ID, count) + + err := builder.ParseExpressionNode(configuration.Name, tagID, tagID, tag.Configuration, field, edge.POSITION) + if err != nil { + return nil, err + } + + field++ + } + } + + return []string{configuration.Name}, nil +} diff --git a/samples/krill/lib/krill/krill_test.go b/samples/krill/lib/krill/krill_test.go new file mode 100644 index 0000000..59e04dd --- /dev/null +++ b/samples/krill/lib/krill/krill_test.go @@ -0,0 +1,448 @@ +package krill + +import ( + "testing" + + "github.com/iot-for-all/device-simulation/components/edge" + "github.com/iot-for-all/device-simulation/components/formatter" + "github.com/iot-for-all/device-simulation/components/node" + "github.com/iot-for-all/device-simulation/components/renderer" + "github.com/iot-for-all/device-simulation/lib/component" + "github.com/stretchr/testify/require" +) + +var ( + MockKrillTag = Tag{ + ID: "float_1", + Configuration: "1", + Count: 1, + MissingChance: 0, + } + MockKrillRate = Rate{ + MessagesPerPeriod: 10, + PeriodSeconds: 1, + TagsPerMessage: 1, + } + MockKrillTarget = Target{ + Host: "localhost", + Port: 1883, + } + MockKrillSite = Site{ + Name: "site0", + Tags: []Tag{ + MockKrillTag, + }, + AssetCount: 1, + Rate: MockKrillRate, + PayloadFormat: "JSON", + TopicFormat: "{{.SiteName}}/{{.AssetName}}", + QoSLevel: 1, + MQTTVersion: "v5", + } + MockKrillConfiguration = Simulation{ + Sites: []Site{ + MockKrillSite, + }, + Target: MockKrillTarget, + } +) + +const ( + MockRootNodeID = "MockRootID" + MockChildNodeID = "MockChildNodeID" + MockEdgeID = "MockEdgeID" + MockNodeExpression = "MockNodeExpression" + MockEdgeConfiguration = "MockEdgeConfiguration" + MockEdgeType = edge.LABEL + MockFormatterID = "MockFormatterID" + MockFormatterType = formatter.BIG_ENDIAN + MockSiteName = "MockSiteName" + MockTagID = "MockTagID" + MockTagConfiguration = "MockTagConfiguration" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestParseExpressionNode(t *testing.T) { + builder := &KrillBuilder{ + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + require.Equal(t, MockChildNodeID, string(identifier)) + require.Equal(t, node.EXPRESSION, entity.Type) + require.Equal(t, MockNodeExpression, entity.Configuration) + return nil + }, + }, edge: &component.MockService[*edge.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *edge.Component) error { + require.Equal(t, MockEdgeID, string(identifier)) + require.Equal(t, MockRootNodeID, string(entity.ParentNodeId)) + require.Equal(t, MockChildNodeID, string(entity.ChildNodeId)) + require.Equal(t, MockEdgeType, entity.Type) + require.Equal(t, MockEdgeConfiguration, entity.Configuration) + return nil + }, + }, + } + + err := builder.ParseExpressionNode(MockRootNodeID, MockChildNodeID, MockEdgeID, MockNodeExpression, MockEdgeConfiguration, MockEdgeType) + require.NoError(t, err) +} + +func TestParseExpressionNodeNodeError(t *testing.T) { + builder := &KrillBuilder{ + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return &component.MockError{} + }, + }, + } + + err := builder.ParseExpressionNode("", "", "", "", "", "") + require.Equal(t, &component.MockError{}, err) +} + +func TestParseCollectionNode(t *testing.T) { + builder := &KrillBuilder{ + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + require.Equal(t, MockChildNodeID, string(identifier)) + require.Equal(t, node.COLLECTION, entity.Type) + require.Equal(t, "", entity.Configuration) + return nil + }, + }, edge: &component.MockService[*edge.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *edge.Component) error { + require.Equal(t, MockEdgeID, string(identifier)) + require.Equal(t, MockRootNodeID, string(entity.ParentNodeId)) + require.Equal(t, MockChildNodeID, string(entity.ChildNodeId)) + require.Equal(t, edge.LABEL, entity.Type) + require.Equal(t, MockEdgeConfiguration, entity.Configuration) + return nil + }, + }, + } + + err := builder.ParseCollectionNode(MockRootNodeID, MockChildNodeID, MockEdgeID, MockEdgeConfiguration) + require.NoError(t, err) +} + +func TestParseCollectionNodeNodeError(t *testing.T) { + builder := &KrillBuilder{ + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return &component.MockError{} + }, + }, + } + + err := builder.ParseCollectionNode("", "", "", "") + require.Equal(t, &component.MockError{}, err) +} + +func TestParseRootNode(t *testing.T) { + builder := &KrillBuilder{ + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + require.Equal(t, MockRootNodeID, string(identifier)) + require.Equal(t, node.COLLECTION, entity.Type) + require.Equal(t, "", entity.Configuration) + return nil + }, + }, renderer: &component.MockService[*renderer.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *renderer.Component) error { + require.Equal(t, MockRootNodeID, string(identifier)) + require.Equal(t, MockFormatterID, string(entity.FormatterID)) + require.Equal(t, MockRootNodeID, string(entity.NodeID)) + return nil + }, + }, + } + + err := builder.ParseRootNode(MockFormatterID, MockRootNodeID, node.COLLECTION) + require.NoError(t, err) +} + +func TestParseRootNodeNodeError(t *testing.T) { + builder := &KrillBuilder{ + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return &component.MockError{} + }, + }, + } + + err := builder.ParseRootNode("", "", "") + require.Equal(t, &component.MockError{}, err) +} + +func TestParseFormatter(t *testing.T) { + builder := &KrillBuilder{ + formatter: &component.MockService[*formatter.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *formatter.Component) error { + require.Equal(t, MockFormatterID, string(identifier)) + require.Equal(t, MockFormatterType, entity.Type) + return nil + }, + }, + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return nil + }, + }, renderer: &component.MockService[*renderer.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *renderer.Component) error { + return nil + }, + }, + } + + err := builder.ParseFormatter(MockFormatterID, MockFormatterType, "") + require.NoError(t, err) +} + +func TestParseFormatterError(t *testing.T) { + builder := &KrillBuilder{ + formatter: &component.MockService[*formatter.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *formatter.Component) error { + return &component.MockError{} + }, + }, + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return nil + }, + }, renderer: &component.MockService[*renderer.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *renderer.Component) error { + return nil + }, + }, + } + + err := builder.ParseFormatter("", "", "") + require.Equal(t, &component.MockError{}, err) +} + +func TestParseFlat(t *testing.T) { + builder := &KrillBuilder{ + formatter: &component.MockService[*formatter.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *formatter.Component) error { + require.Equal(t, MockFormatterType, entity.Type) + require.Equal(t, MockSiteName, string(identifier)) + return nil + }, + }, + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return nil + }, + }, edge: &component.MockService[*edge.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *edge.Component) error { + return nil + }, + }, renderer: &component.MockService[*renderer.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *renderer.Component) error { + return nil + }, + }, + } + + tags, err := builder.ParseFlat(Site{ + Name: MockSiteName, + Tags: []Tag{ + { + ID: MockTagID, + Configuration: MockTagConfiguration, + Count: 1, + }, + }, + }, MockFormatterType) + require.NoError(t, err) + require.Equal(t, []string{MockSiteName}, tags) +} + +func TestParseFlatFormatterError(t *testing.T) { + builder := &KrillBuilder{ + formatter: &component.MockService[*formatter.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *formatter.Component) error { + return &component.MockError{} + }, + }, + } + + _, err := builder.ParseFlat(Site{}, MockFormatterType) + require.Equal(t, &component.MockError{}, err) +} + +func TestParseFlatExpressionNodeError(t *testing.T) { + builder := &KrillBuilder{ + formatter: &component.MockService[*formatter.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *formatter.Component) error { + return nil + }, + }, + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return nil + }, + }, edge: &component.MockService[*edge.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *edge.Component) error { + return &component.MockError{} + }, + }, renderer: &component.MockService[*renderer.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *renderer.Component) error { + return nil + }, + }, + } + + _, err := builder.ParseFlat(Site{ + Name: MockSiteName, + Tags: []Tag{ + { + Count: 1, + }, + }, + }, MockFormatterType) + require.Equal(t, &component.MockError{}, err) +} + +func TestParseJSONTag(t *testing.T) { + builder := &KrillBuilder{ + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return nil + }, + }, edge: &component.MockService[*edge.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *edge.Component) error { + require.Equal(t, "MockSiteName__MockTagID__0__child", string(entity.ChildNodeId)) + return nil + }, + }, renderer: &component.MockService[*renderer.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *renderer.Component) error { + return nil + }, + }, + } + + res, err := builder.ParseJSONTag(MockSiteName, Tag{ + Count: 1, + ID: MockTagID, + }, 0) + require.NoError(t, err) + require.Equal(t, "MockSiteName__MockTagID__0", res) +} + +func TestParseJSONTagRootNodeError(t *testing.T) { + builder := &KrillBuilder{ + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return &component.MockError{} + }, + }, renderer: &component.MockService[*renderer.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *renderer.Component) error { + return nil + }, + }, + } + + _, err := builder.ParseJSONTag(MockSiteName, Tag{}, 0) + require.Equal(t, &component.MockError{}, err) +} + +func TestParseJSONTagExpressionNodeError(t *testing.T) { + builder := &KrillBuilder{ + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return nil + }, + }, edge: &component.MockService[*edge.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *edge.Component) error { + return &component.MockError{} + }, + }, renderer: &component.MockService[*renderer.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *renderer.Component) error { + return nil + }, + }, + } + + _, err := builder.ParseJSONTag(MockSiteName, Tag{}, 0) + require.Equal(t, &component.MockError{}, err) +} + +func TestParseJSONTagPerMessage(t *testing.T) { + builder := &KrillBuilder{ + formatter: &component.MockService[*formatter.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *formatter.Component) error { + require.Equal(t, MockSiteName, string(identifier)) + require.Equal(t, formatter.JSON, entity.Type) + return nil + }, + }, + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return nil + }, + }, edge: &component.MockService[*edge.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *edge.Component) error { + return nil + }, + }, renderer: &component.MockService[*renderer.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *renderer.Component) error { + return nil + }, + }, + } + + tags, err := builder.ParseJSONTagPerMessage(Site{ + Name: MockSiteName, + Tags: []Tag{ + { + ID: MockTagID, + Count: 1, + }, + }, + }) + require.NoError(t, err) + require.Equal(t, []string{"MockSiteName__MockTagID__0"}, tags) +} + +func TestParseJSONTagPerMessageFormatterError(t *testing.T) { + builder := &KrillBuilder{ + formatter: &component.MockService[*formatter.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *formatter.Component) error { + return &component.MockError{} + }, + }, + } + + _, err := builder.ParseJSONTagPerMessage(Site{}) + require.Equal(t, &component.MockError{}, err) +} + +func TestParseJSONTagPerMessageParseJSONTagError(t *testing.T) { + builder := &KrillBuilder{ + formatter: &component.MockService[*formatter.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *formatter.Component) error { + return nil + }, + }, + node: &component.MockService[*node.Component, component.ID]{ + OnCreate: func(identifier component.ID, entity *node.Component) error { + return &component.MockError{} + }, + }, + } + + _, err := builder.ParseJSONTagPerMessage(Site{ + Name: MockSiteName, + Tags: []Tag{ + { + ID: MockTagID, + Count: 1, + }, + }, + }) + require.Equal(t, &component.MockError{}, err) +} \ No newline at end of file diff --git a/samples/krill/lib/logger/logger.go b/samples/krill/lib/logger/logger.go new file mode 100644 index 0000000..e750d51 --- /dev/null +++ b/samples/krill/lib/logger/logger.go @@ -0,0 +1,210 @@ +// Package logger provides all logging functionality for the simulation framework. +package logger + +import ( + "fmt" + + "github.com/rs/zerolog" +) + +// Level provides the String function which converts an int-based level into a string representation. +type Level struct{} + +const ( + Trace int = iota + Debug + Info + Warn + Critical + Error + Fatal + Panic +) + +const ( + levelName = "level" +) + +// String converts a given integer level to a string representation. +// It is meant to be used with the iota enumeration defined above. +func (l *Level) String(level int) string { + switch level { + case Trace: + return "trace" + case Debug: + return "debug" + case Info: + return "info" + case Warn: + return "warn" + case Critical: + return "critical" + case Error: + return "error" + case Fatal: + return "fatal" + case Panic: + return "panic" + default: + return "no_level_defined" + } +} + +// LevelString is an interface whose implementation should convert an integer based log level to a string. +type LevelString interface { + String(int) string +} + +// Logger is an interface whose implementation should be able to print logs and enrich these logs with level information and custom key-value pair fields. +type Logger interface { + Println(v ...interface{}) + Printf(format string, v ...interface{}) + Level(l int) Logger + With(k, v string) Logger + Tag(t string) Logger +} + +// ZeroLoggerWrapper is a Logger implementation which wraps the logging functionality of the zero-log library. +// It also uses a LevelString implementation to convert numerical log levels to strings which can be logged. +type ZeroLoggerWrapper struct { + logger zerolog.Logger + Levels LevelString + LogLevel int + Tags string +} + +// NewZeroLoggerWrapper creates a new ZeroLoggerWrapper given a zerolog logger. +// Optional parameters (LevelString implementation and LogLevel) can be set using the options function. +func NewZeroLoggerWrapper( + logger zerolog.Logger, + options ...func(*ZeroLoggerWrapper), +) *ZeroLoggerWrapper { + wrapper := &ZeroLoggerWrapper{ + logger: logger, + Levels: &Level{}, + } + + for _, option := range options { + option(wrapper) + } + + return wrapper +} + +// Level creates a new ZeroLoggerWrapper whose logs will all contain the level field defined by the provided level. +// If the level is not above the originally specified log level, a noop logger will be returned. +func (loggerWrapper *ZeroLoggerWrapper) Level(l int) Logger { + if l < loggerWrapper.LogLevel { + return &NoopLogger{} + } + + return &ZeroLoggerWrapper{ + logger: loggerWrapper.logger.With(). + Str(levelName, loggerWrapper.Levels.String(l)). + Logger(), + Levels: loggerWrapper.Levels, + LogLevel: loggerWrapper.LogLevel, + Tags: loggerWrapper.Tags, + } +} + +// With enriches the current logger with a new key value pair, and returns a new ZeroLoggerWrapper. +func (loggerWrapper *ZeroLoggerWrapper) With(k, v string) Logger { + return &ZeroLoggerWrapper{ + logger: loggerWrapper.logger.With().Str(k, v).Logger(), + Levels: loggerWrapper.Levels, + LogLevel: loggerWrapper.LogLevel, + Tags: loggerWrapper.Tags, + } +} + +func (loggerWrapper *ZeroLoggerWrapper) Tag(t string) Logger { + if loggerWrapper.Tags == "" { + return &ZeroLoggerWrapper{ + logger: loggerWrapper.logger, + Levels: loggerWrapper.Levels, + LogLevel: loggerWrapper.LogLevel, + Tags: `"` + t + `"`, + } + } + return &ZeroLoggerWrapper{ + logger: loggerWrapper.logger, + Levels: loggerWrapper.Levels, + LogLevel: loggerWrapper.LogLevel, + Tags: loggerWrapper.Tags + `,"` + t + `"`, + } +} + +// Printf converts a format string using Sprintf and then logs this as the message field in the zerolog logger. +func (loggerWrapper *ZeroLoggerWrapper) Printf( + format string, + v ...interface{}, +) { + loggerWrapper.logger.Log().RawJSON("tags", []byte("[" + loggerWrapper.Tags + "]")).Msg(fmt.Sprintf(format, v...)) +} + +// Println applies a Sprintln to provided parameters and then logs this as a message field in the zerolog logger. +func (loggerWrapper *ZeroLoggerWrapper) Println(v ...interface{}) { + loggerWrapper.logger.Log().RawJSON("tags", []byte("[" + loggerWrapper.Tags + "]")).Msg(fmt.Sprintln(v...)) +} + +// NoopLogger is a Logger implementation with Noop functionality. +// From a performance perspective, it makes the most sense to perform logging this way, +// as the compiler will optimize noop function calls under the hood. +type NoopLogger struct { +} + +// Println is a noop. +func (*NoopLogger) Println(...interface{}) { +} + +// Printf is a noop. +func (*NoopLogger) Printf(string, ...interface{}) { +} + +// Level returns the noop logger. +func (l *NoopLogger) Level(int) Logger { + return l +} + +// With returns the noop logger. +func (l *NoopLogger) With(string, string) Logger { + return l +} + +func (l *NoopLogger) Tag(t string) Logger { + return l +} + +// MockLogger is a mock logger implementation to be used in testing scenarios. +type MockLogger struct { + OnPrintln func(...interface{}) + OnPrintf func(string, ...interface{}) + OnLevel func(int) Logger + OnWith func(string, string) Logger + OnTag func(string) Logger +} + +// Println calls the mock logger's OnPrintln function, passing through its parameters. +func (l *MockLogger) Println(s ...interface{}) { + l.OnPrintln(s...) +} + +// Printf calls the mock logger's OnPrintf function, passing through its parameters. +func (l *MockLogger) Printf(s string, ls ...interface{}) { + l.OnPrintf(s, ls...) +} + +// Level calls the mock logger's OnLevel function, passing through its parameters. +func (l *MockLogger) Level(i int) Logger { + return l.OnLevel(i) +} + +// With calls the mock logger's OnWith function, passing through its parameters. +func (l *MockLogger) With(k string, v string) Logger { + return l.OnWith(k, v) +} + +func (l *MockLogger) Tag(t string) Logger { + return l.OnTag(t) +} diff --git a/samples/krill/lib/logger/logger_test.go b/samples/krill/lib/logger/logger_test.go new file mode 100644 index 0000000..6531494 --- /dev/null +++ b/samples/krill/lib/logger/logger_test.go @@ -0,0 +1,44 @@ +package logger + +import ( + "testing" + + "github.com/rs/zerolog/log" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestZeroLoggerWrapper(t *testing.T) { + wrappedLogger := NewZeroLoggerWrapper(log.With().Logger()) + wrappedLogger.Printf("") + wrappedLogger.Println("") +} + +func TestNoopLogger(t *testing.T) { + logger := &NoopLogger{} + logger.Printf("") + logger.Println("") + logger.With("", "") + logger.Level(0) +} + +func TestMockLogger(t *testing.T) { + logger := &MockLogger{ + OnPrintln: func(...interface{}) { + }, + OnPrintf: func(string, ...interface{}) { + }, + OnLevel: func(int) Logger { + return nil + }, + OnWith: func(string, string) Logger { + return nil + }, + } + logger.Printf("") + logger.Println("") + logger.With("", "") + logger.Level(0) +} \ No newline at end of file diff --git a/samples/krill/lib/proto/gen.sh b/samples/krill/lib/proto/gen.sh new file mode 100755 index 0000000..6e4f96e --- /dev/null +++ b/samples/krill/lib/proto/gen.sh @@ -0,0 +1,3 @@ +protoc --go_out=. --go_opt=paths=source_relative \ + --go-grpc_out=require_unimplemented_servers=false:. --go-grpc_opt=paths=source_relative \ + message.proto \ No newline at end of file diff --git a/samples/krill/lib/proto/message.pb.go b/samples/krill/lib/proto/message.pb.go new file mode 100644 index 0000000..dd3656d --- /dev/null +++ b/samples/krill/lib/proto/message.pb.go @@ -0,0 +1,246 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.6.1 +// source: message.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Message struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Options: + // + // *Message_String_ + // *Message_Integer + // *Message_Float + // *Message_Boolean + Options isMessage_Options `protobuf_oneof:"options"` + Map map[string]*Message `protobuf:"bytes,5,rep,name=map,proto3" json:"map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Array []*Message `protobuf:"bytes,6,rep,name=array,proto3" json:"array,omitempty"` +} + +func (x *Message) Reset() { + *x = Message{} + if protoimpl.UnsafeEnabled { + mi := &file_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_message_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_message_proto_rawDescGZIP(), []int{0} +} + +func (m *Message) GetOptions() isMessage_Options { + if m != nil { + return m.Options + } + return nil +} + +func (x *Message) GetString_() string { + if x, ok := x.GetOptions().(*Message_String_); ok { + return x.String_ + } + return "" +} + +func (x *Message) GetInteger() int32 { + if x, ok := x.GetOptions().(*Message_Integer); ok { + return x.Integer + } + return 0 +} + +func (x *Message) GetFloat() float64 { + if x, ok := x.GetOptions().(*Message_Float); ok { + return x.Float + } + return 0 +} + +func (x *Message) GetBoolean() bool { + if x, ok := x.GetOptions().(*Message_Boolean); ok { + return x.Boolean + } + return false +} + +func (x *Message) GetMap() map[string]*Message { + if x != nil { + return x.Map + } + return nil +} + +func (x *Message) GetArray() []*Message { + if x != nil { + return x.Array + } + return nil +} + +type isMessage_Options interface { + isMessage_Options() +} + +type Message_String_ struct { + String_ string `protobuf:"bytes,1,opt,name=string,proto3,oneof"` +} + +type Message_Integer struct { + Integer int32 `protobuf:"varint,2,opt,name=integer,proto3,oneof"` +} + +type Message_Float struct { + Float float64 `protobuf:"fixed64,3,opt,name=float,proto3,oneof"` +} + +type Message_Boolean struct { + Boolean bool `protobuf:"varint,4,opt,name=boolean,proto3,oneof"` +} + +func (*Message_String_) isMessage_Options() {} + +func (*Message_Integer) isMessage_Options() {} + +func (*Message_Float) isMessage_Options() {} + +func (*Message_Boolean) isMessage_Options() {} + +var File_message_proto protoreflect.FileDescriptor + +var file_message_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0x85, 0x02, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x06, 0x73, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x73, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x1a, 0x0a, 0x07, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x07, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x65, + 0x72, 0x12, 0x16, 0x0a, 0x05, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, + 0x48, 0x00, 0x52, 0x05, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x12, 0x1a, 0x0a, 0x07, 0x62, 0x6f, 0x6f, + 0x6c, 0x65, 0x61, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x62, 0x6f, + 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x12, 0x23, 0x0a, 0x03, 0x6d, 0x61, 0x70, 0x18, 0x05, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x4d, 0x61, 0x70, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x6d, 0x61, 0x70, 0x12, 0x1e, 0x0a, 0x05, 0x61, 0x72, + 0x72, 0x61, 0x79, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x05, 0x61, 0x72, 0x72, 0x61, 0x79, 0x1a, 0x40, 0x0a, 0x08, 0x4d, 0x61, + 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x6f, 0x74, 0x2d, 0x66, 0x6f, 0x72, 0x2d, 0x61, 0x6c, + 0x6c, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2d, 0x73, 0x69, 0x6d, 0x75, 0x6c, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x2f, 0x6c, 0x69, 0x62, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_message_proto_rawDescOnce sync.Once + file_message_proto_rawDescData = file_message_proto_rawDesc +) + +func file_message_proto_rawDescGZIP() []byte { + file_message_proto_rawDescOnce.Do(func() { + file_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_message_proto_rawDescData) + }) + return file_message_proto_rawDescData +} + +var file_message_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_message_proto_goTypes = []interface{}{ + (*Message)(nil), // 0: Message + nil, // 1: Message.MapEntry +} +var file_message_proto_depIdxs = []int32{ + 1, // 0: Message.map:type_name -> Message.MapEntry + 0, // 1: Message.array:type_name -> Message + 0, // 2: Message.MapEntry.value:type_name -> Message + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_message_proto_init() } +func file_message_proto_init() { + if File_message_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_message_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*Message_String_)(nil), + (*Message_Integer)(nil), + (*Message_Float)(nil), + (*Message_Boolean)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_message_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_message_proto_goTypes, + DependencyIndexes: file_message_proto_depIdxs, + MessageInfos: file_message_proto_msgTypes, + }.Build() + File_message_proto = out.File + file_message_proto_rawDesc = nil + file_message_proto_goTypes = nil + file_message_proto_depIdxs = nil +} diff --git a/samples/krill/lib/proto/message.proto b/samples/krill/lib/proto/message.proto new file mode 100644 index 0000000..a7af10d --- /dev/null +++ b/samples/krill/lib/proto/message.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; +option go_package = "github.com/iot-for-all/device-simulation/lib/proto"; + +message Message { + oneof options { + string string = 1; + int32 integer = 2; + double float = 3; + bool boolean = 4; + } + map map = 5; + repeated Message array = 6; +} \ No newline at end of file diff --git a/samples/krill/lib/proto/proto.go b/samples/krill/lib/proto/proto.go new file mode 100644 index 0000000..6328000 --- /dev/null +++ b/samples/krill/lib/proto/proto.go @@ -0,0 +1,100 @@ +package proto + +type Encoder interface { + Encode(any) *Message + Decode(*Message) any +} + +type ProtoEncoder struct { +} + +func New() *ProtoEncoder { + return &ProtoEncoder{} +} + +func (encoder *ProtoEncoder) Encode(message any) *Message { + + res := new(Message) + + switch option := message.(type) { + case []any: + for _, element := range option { + res.Array = append(res.Array, encoder.Encode(element)) + } + case map[string]any: + + res.Map = make(map[string]*Message) + + for k, v := range option { + res.Map[k] = encoder.Encode(v) + } + case int: + res.Options = &Message_Integer{ + Integer: int32(option), + } + case float64: + res.Options = &Message_Float{ + Float: option, + } + case string: + res.Options = &Message_String_{ + String_: option, + } + case bool: + res.Options = &Message_Boolean{ + Boolean: option, + } + } + + return res +} + +func (encoder *ProtoEncoder) Decode(message *Message) any { + + if len(message.Array) > 0 { + + res := make([]any, len(message.Array)) + + for idx, element := range message.Array { + res[idx] = encoder.Decode(element) + } + + return res + } + + if len(message.Map) > 0 { + res := make(map[string]any) + + for k, v := range message.Map { + res[k] = encoder.Decode(v) + } + + return res + } + + switch option := message.Options.(type) { + case *Message_String_: + return option.String_ + case *Message_Integer: + return int(option.Integer) + case *Message_Float: + return option.Float + case *Message_Boolean: + return option.Boolean + } + + return nil +} + +type MockEncoder struct { + OnEncode func(any) *Message + OnDecode func(*Message) any +} + +func (encoder *MockEncoder) Encode(a any) *Message { + return encoder.OnEncode(a) +} + +func (encoder *MockEncoder) Decode(a *Message) any { + return encoder.OnDecode(a) +} diff --git a/samples/krill/lib/proto/proto_test.go b/samples/krill/lib/proto/proto_test.go new file mode 100644 index 0000000..6966413 --- /dev/null +++ b/samples/krill/lib/proto/proto_test.go @@ -0,0 +1,36 @@ +package proto + +import ( + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestMain(m *testing.M) { + m.Run() +} + +func TestProtoEncodingAndDecoding(t *testing.T) { + encoder := New() + + original := map[string]any{ + "my_arr": []any{"1", 2, 3.0, true}, + "my_map": map[string]any{"hello": "hello", "hello-1": 1}, + "my_elem": 66.0, + } + + message := encoder.Encode(original) + + b, err := proto.Marshal(message) + require.NoError(t, err) + + res := new(Message) + + err = proto.Unmarshal(b, res) + require.NoError(t, err) + + decoded := encoder.Decode(res) + + require.Equal(t, original, decoded) +} diff --git a/samples/krill/lib/templater/templater.go b/samples/krill/lib/templater/templater.go new file mode 100755 index 0000000..7021fc1 --- /dev/null +++ b/samples/krill/lib/templater/templater.go @@ -0,0 +1,85 @@ +// Package templater contains the templating functionality used to convert a general request body template into a body specific to a particular request. +package templater + +import ( + "bytes" + "io" + "text/template" +) + +// TemplateRenderer is an interface with a Render method. +// Render converts a generic to an io.Reader, or an error. +type TemplateRenderer[T any] interface { + Render(vars T) (io.Reader, error) +} + +type TemplateExecuter interface { + Execute(wr io.Writer, data any) error +} + +type Executor struct { + *template.Template +} + +func NewExecutor(content string) (*Executor, error) { + templ, err := template.New("").Parse(content) + if err != nil { + return nil, err + } + return &Executor{ + templ, + }, nil +} + +// Templater is a wrapper struct around the functionality of the golang wrapper. +type Templater[T any] struct { + template TemplateExecuter +} + +// New creates a new Templater. +// It must be given a template in the form of a string as a parameter. +func New[T any](templ TemplateExecuter) *Templater[T] { + return &Templater[T]{ + template: templ, + } +} + +// Render converts a template into an io.Reader (which can be further reduced to string) given variables specific to that template. +func (templater *Templater[T]) Render(vars T) (io.Reader, error) { + var buffer bytes.Buffer + err := templater.template.Execute(&buffer, vars) + if err != nil { + return nil, err + } + + return &buffer, nil +} + +type NoopReader struct{} + +func (*NoopReader) Read([]byte) (int, error) { return 0, io.EOF } +func (*NoopReader) Close() error { return nil } +func (*NoopReader) WriteTo(io.Writer) (int64, error) { return 0, nil } + +type NoopRenderer[T any] struct { +} + +func (renderer *NoopRenderer[T]) Render(vars T) (io.Reader, error) { + return &NoopReader{}, nil +} + +type MockRenderer[T any] struct { + OnRender func(vars T) (io.Reader, error) +} + +func (renderer *MockRenderer[T]) Render(vars T) (io.Reader, error) { + return renderer.OnRender(vars) +} + +type MockExecutor struct { + OnExecute func(wr io.Writer, data any) error +} + +func (executor *MockExecutor) Execute(wr io.Writer, data any) error { + return executor.OnExecute(wr, data) +} \ No newline at end of file diff --git a/samples/krill/lib/templater/templater_test.go b/samples/krill/lib/templater/templater_test.go new file mode 100755 index 0000000..f94bc73 --- /dev/null +++ b/samples/krill/lib/templater/templater_test.go @@ -0,0 +1,105 @@ +package templater + +import ( + "io" + "testing" + "text/template" + + "github.com/iot-for-all/device-simulation/lib/errors" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + m.Run() +} + +type MockType struct { + ID string +} + +var ( + example = `{"id": {{ .ID }}}` + expected = `{"id": 1}` + id = "1" +) + +func TestTemplater(t *testing.T) { + templ, err := template.New("").Parse(example) + require.NoError(t, err) + + templater := New[MockType](templ) + reader, err := templater.Render(MockType{ + ID: id, + }) + require.NoError(t, err) + + res, err := io.ReadAll(reader) + require.NoError(t, err) + + require.Equal(t, expected, string(res)) +} + +func TestTemplaterRenderError(t *testing.T) { + + templater := New[MockType](&MockExecutor{ + OnExecute: func(wr io.Writer, data any) error { + return errors.Mock{} + }, + }) + _, err := templater.Render(MockType{ + ID: id, + }) + require.Equal(t, errors.Mock{}, err) +} + +func TestNoopRenderer(t *testing.T) { + noopRenderer := &NoopRenderer[MockType]{} + reader, err := noopRenderer.Render(MockType{}) + require.NoError(t, err) + require.Equal(t, &NoopReader{}, reader) +} + +func TestNoopReader(t *testing.T) { + reader := &NoopReader{} + + readRes, err := reader.Read(nil) + require.Equal(t, 0, readRes) + require.Equal(t, io.EOF, err) + + require.NoError(t, reader.Close()) + + writeRes, err := reader.WriteTo(nil) + require.Equal(t, int64(0), writeRes) + require.NoError(t, err) +} + +func TestMockRenderer(t *testing.T) { + mockRenderer := &MockRenderer[MockType]{ + OnRender: func(vars MockType) (io.Reader, error) { + return nil, errors.Mock{} + }, + } + + _, err := mockRenderer.Render(MockType{}) + require.Equal(t, errors.Mock{}, err) +} + +func TestMockExecutor(t *testing.T) { + mockExecutor := &MockExecutor{ + OnExecute: func(wr io.Writer, data any) error { + return errors.Mock{} + }, + } + + require.Equal(t, errors.Mock{}, mockExecutor.Execute(nil, nil)) +} + +func TestExecutor(t *testing.T) { + _, err := NewExecutor("") + require.NoError(t, err) +} + +func TestExecutorParseError(t *testing.T) { + _, err := NewExecutor("{{{}}") + require.Error(t, err) +} \ No newline at end of file From 085606a87390aa2f28453edb3a3bf85152e0d0e7 Mon Sep 17 00:00:00 2001 From: Ethan Date: Tue, 7 Nov 2023 14:29:24 -0800 Subject: [PATCH 4/5] Update krill readme --- samples/krill/README.md | 21 ++++++++++++--------- samples/krill/configs/simple/config.yml | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/samples/krill/README.md b/samples/krill/README.md index 0a34b7c..e7cb586 100644 --- a/samples/krill/README.md +++ b/samples/krill/README.md @@ -18,21 +18,23 @@ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠙⠛⠛⠛⠛⠋⠉⠉⠁ ``` -Krill is a command line utility for simulating MQTT assets. +Krill is a highly configurable MQTT asset simulator. ## Usage -`./krill [--config config_file_path] [--yaml yaml_format_bool]` +### Krill as K8s Pod -Config will be supplied with the default value of "config.yml", and the yaml boolean will be set to true. If the yaml boolean is set to false, krill will attempt to decode using JSON instead of YAML. +`kubectl run krill --image=azbluefin.azurecr.io/krill:latest --stdin < config.yml` + +Krill runs as a pod within the desired cluster using the kubectl run command. The configuration of choice is provided via stdin of the krill process within the pod, provided in the command above using `--stdin < config.yml`. Alternative names for a configuration file may be used -- the command simply uses stdin piping to provide krill with an appropriate configuration. See the example YAML configuration below, with comments describing the various configurable fields. -``` -ports: - metrics: 2113 # Port to host prometheus formatted metrics. - refData: 2114 # Application server port for ref data. -logLevel: 5 # Log level (debug: 0, info: 1, warn: 2, error: 3, critical: 4, fatal: 5, panic: 6) +```yaml +metrics: + type: prometheus # Type of metrics (prometheus is the only current option). + port: 2114 # Port to host prometheus formatted metrics. +logLevel: 5 # Log level (trace: 0, debug: 1, info: 2, warn: 3, error: 4, critical: 5, fatal: 6, panic: 7) simulation: target: # Target broker information. endpoint: localhost @@ -85,6 +87,7 @@ simulation: payloadFormat: JSON # Format of message (JSON, JSONTagPerMessage, BigEndian, LittleEndian) topicFormat: "{{.SiteName}}/{{.AssetName}}/{{.TagName}}" # Format of topic(s). qosLevel: 1 # QoS level of published messages. + mqttVersion: v5 # MQTT protocol version to use for clients in this site (v3 or v5 permitted). ``` @@ -178,4 +181,4 @@ Example Equations: Prometheus metrics are provided by the krill simulator at the port specified in the metrics field of the configuration. The available metrics are: 1. `krill_entity_gauge` - shows the count of each system entity. -1. `krill__asset_publish_counter` - records the number of messages published, labeled by asset identifier. +1. `krill__asset_publish_counter` - records the number of messages published, labeled by asset identifier. \ No newline at end of file diff --git a/samples/krill/configs/simple/config.yml b/samples/krill/configs/simple/config.yml index b4105c5..5e38662 100644 --- a/samples/krill/configs/simple/config.yml +++ b/samples/krill/configs/simple/config.yml @@ -8,7 +8,7 @@ simulation: port: 1883 sites: - name: site0 - mqttVersion: v3 + mqttVersion: v5 assetCount: 3 tags: - id: float_1 From 8232250587dd909dbe31cc1081d86a7564a7d9ac Mon Sep 17 00:00:00 2001 From: Ethan Date: Tue, 7 Nov 2023 15:32:05 -0800 Subject: [PATCH 5/5] Add callout tool --- .github/workflows/docker_build.yml | 1 - samples/callout/.gitignore | 24 + samples/callout/.vscode/launch.json | 19 + samples/callout/Dockerfile | 23 + samples/callout/README.md | 76 ++++ samples/callout/cmd/config.go | 23 + samples/callout/cmd/main.go | 148 ++++++ samples/callout/go.mod | 37 ++ samples/callout/go.sum | 502 +++++++++++++++++++++ samples/callout/pkg/models/quality.go | 25 + samples/callout/pkg/serving/admin.go | 39 ++ samples/callout/pkg/serving/echoRequest.go | 38 ++ samples/callout/pkg/serving/qFactor.go | 60 +++ samples/callout/setup/service.yaml | 42 ++ 14 files changed, 1056 insertions(+), 1 deletion(-) create mode 100644 samples/callout/.gitignore create mode 100644 samples/callout/.vscode/launch.json create mode 100644 samples/callout/Dockerfile create mode 100644 samples/callout/README.md create mode 100644 samples/callout/cmd/config.go create mode 100644 samples/callout/cmd/main.go create mode 100644 samples/callout/go.mod create mode 100644 samples/callout/go.sum create mode 100644 samples/callout/pkg/models/quality.go create mode 100644 samples/callout/pkg/serving/admin.go create mode 100644 samples/callout/pkg/serving/echoRequest.go create mode 100644 samples/callout/pkg/serving/qFactor.go create mode 100644 samples/callout/setup/service.yaml diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 80abf8e..4e175ae 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -10,7 +10,6 @@ jobs: docker_build: name: 'Build docker containers' runs-on: ubuntu-latest - environment: production defaults: run: diff --git a/samples/callout/.gitignore b/samples/callout/.gitignore new file mode 100644 index 0000000..7826168 --- /dev/null +++ b/samples/callout/.gitignore @@ -0,0 +1,24 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ +bin/logs + +# Go workspace file +go.work + +bin \ No newline at end of file diff --git a/samples/callout/.vscode/launch.json b/samples/callout/.vscode/launch.json new file mode 100644 index 0000000..0e7f7d5 --- /dev/null +++ b/samples/callout/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "type": "go", + "request": "launch", + "mode": "debug", + "cwd": "${workspaceRoot}/bin", + "program": "${workspaceRoot}", + "env": {}, + "args": [], + "showLog": true + } + ] +} \ No newline at end of file diff --git a/samples/callout/Dockerfile b/samples/callout/Dockerfile new file mode 100644 index 0000000..04ea75a --- /dev/null +++ b/samples/callout/Dockerfile @@ -0,0 +1,23 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.21-alpine + +# Set destination for COPY +WORKDIR /app +ADD . /app + +# Download Go modules +RUN go mod download + +# Build +RUN CGO_ENABLED=0 GOOS=linux go build -o /callout + +# Optional: +# To bind to a TCP port, runtime parameters must be supplied to the docker command. +# But we can document in the Dockerfile what ports +# the application is going to listen on by default. +# https://docs.docker.com/engine/reference/builder/#expose +EXPOSE 8080 + +# Run +CMD ["/callout"] \ No newline at end of file diff --git a/samples/callout/README.md b/samples/callout/README.md new file mode 100644 index 0000000..f816e63 --- /dev/null +++ b/samples/callout/README.md @@ -0,0 +1,76 @@ +# callout +This is a HTTP endpoint that can be used to debug or process data in AIO Data Processor using **Call out HTTP**. + +## Usage + +### Deploy container to Kubernetes cluster + +This utility can be deployed as a service in your Kubernetes cluster. You can use the container that is published to ACR(Azure Container Registry) using [setup/service.yaml](setup/service.yaml) . +``` +kubectl apply -f setup/service.yaml +``` + +### Debugging pipeline +Data Processor has a **Call out HTTP** stage where you can call a HTTP endpoint from with in the pipeline. In that callout stage, you can use the api/echo route to print the contents of the message. Where ever you need to see the message, you can add a callout stage. + +|Parameter | Value | Description | +|----------|-------------|--------------| +| Method | GET or POST | any payload sent in the body is printed as pretty JSON | +| URL | http://callout.default.svc.cluster.local/api/echo/myStage | The URL of the callout endpoint hosted in the cluster. To disambiguate the print outputs, you can use a string like *myStage* or *stage2* etc. | + +### Quality factor +You can compute quality factor using a **Call out HTTP** stage hitting this HTTP endpoint. In that callout stage, you can use the api/qfactor route to comput qFactor, Quality and shift. + +|Parameter | Value | Description | +|----------|-------------|--------------| +| Method | POST | | +| URL | http://callout.default.svc.cluster.local/api/qfactor | | + +#### Input Message #### +```JSON +{ + "Payload": { + "age": 14, + "asset_id": "Red_S1", + "asset_name": "Redmond_Slicer_Redmond_Slicer__asset_0", + "country": "USA", + "humidity": 94.49016579867568, + "id": "Red_S1", + "machine_status": 0, + "operating_time": 12527, + "product": "Takis", + "site": "Redmond", + "source_timestamp": "2023-10-18T18:07:45.575Z", + "temperature": 91.06476575011023, + "vibration": 45.53238287505511 + }, + "SequenceNumber": 12515, + "Timestamp": "2023-10-18T11:07:45.566556393-07:00" +} +``` +#### Output Message #### +```JSON +{ + "Payload": { + "age": 14, + "asset_id": "Red_S1", + "asset_name": "Redmond_Slicer_Redmond_Slicer__asset_0", + "country": "USA", + "humidity": 94.49016579867568, + "id": "Red_S1", + "machine_status": 0, + "operating_time": 12527, + "pressure": 0, + "product": "Takis", + "site": "Redmond", + "temperature": 91.06476575011023, + "vibration": 45.53238287505511, + "q_factor": 0.8, + "quality": "Good", + "shift": 3, + "source_timestamp": "2023-10-18T18:07:45.575Z" + }, + "SequenceNumber": 12515, + "Timestamp": "2023-10-18T11:07:45.566556393-07:00" +} +``` diff --git a/samples/callout/cmd/config.go b/samples/callout/cmd/config.go new file mode 100644 index 0000000..435771f --- /dev/null +++ b/samples/callout/cmd/config.go @@ -0,0 +1,23 @@ +package main + +type ( + Config struct { + LogLevel string `json:"logLevel"` // logging level for the application + LogsDir string `json:"logsDir"` // directory into which logs are written + } + + config struct { + Logger Config `json:"logger"` + Port int `json:"port"` + } +) + +func newConfig() *config { + return &config{ + Logger: Config{ + LogLevel: "Debug", + LogsDir: "./logs", + }, + Port: 8888, + } +} diff --git a/samples/callout/cmd/main.go b/samples/callout/cmd/main.go new file mode 100644 index 0000000..0caee84 --- /dev/null +++ b/samples/callout/cmd/main.go @@ -0,0 +1,148 @@ +package main + +import ( + "context" + _ "embed" + "fmt" + "io" + "os" + "os/signal" + "path" + "strings" + + "github.com/reddydMSFT/callout/pkg/serving" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + "gopkg.in/natefinch/lumberjack.v2" +) + +func main() { + // handle process exit gracefully + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt) + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + defer func() { + // Close the os signal channel to prevent any leak. + signal.Stop(sig) + }() + + // load configuration and initialize logger + cfg, err := loadConfig() + if err != nil { + panic(fmt.Errorf("failed to initialize configuration. %w", err)) + } + initLogger(cfg) + + go serving.StartAdmin(cfg.Port) + + // Wait signal / cancellation + <-sig + + cancel() // Wait for device to completely shut down. +} + +// loadConfig loads the configuration file +func loadConfig() (*config, error) { + colorReset := "\033[0m" + //colorRed := "\033[31m" + colorGreen := "\033[32m" + //colorYellow := "\033[33m" + colorBlue := "\033[34m" + //colorPurple := "\033[35m" + //colorCyan := "\033[36m" + //colorWhite := "\033[37m" + fmt.Printf(string(colorGreen)) + fmt.Printf(` + ██████╗ █████╗ ██╗ ██╗ ██████╗ ██╗ ██╗████████╗ +██╔════╝██╔══██╗██║ ██║ ██╔═══██╗██║ ██║╚══██╔══╝ +██║ ███████║██║ ██║ ██║ ██║██║ ██║ ██║ +██║ ██╔══██║██║ ██║ ██║ ██║██║ ██║ ██║ +╚██████╗██║ ██║███████╗███████╗╚██████╔╝╚██████╔╝ ██║ + ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ +`) + fmt.Printf(string(colorBlue)) + fmt.Printf(" AIO DATA PROCESSOR CALLOUT\n") + fmt.Printf(string(colorReset)) + + viper.SetConfigName("callout") + viper.SetConfigType("json") + viper.AddConfigPath(".") + viper.AddConfigPath("./bin") + + viper.AutomaticEnv() + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + fmt.Print(`Add a configuration file (callout.json) with the file contents below: + +{ + "logger": { + "logLevel": "Debug", + "logsDir": "./logs" + }, + "port": 8888 +} + +\n`) + return nil, err + } + } + + cfg := newConfig() + if err := viper.Unmarshal(cfg); err != nil { + return nil, err + } + + //fmt.Printf("loaded configuration from %s\n", viper.ConfigFileUsed()) + return cfg, nil +} + +// initLogger initializes the logger with output format +func initLogger(cfg *config) { + var writers []io.Writer + writers = append(writers, zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"}) + + fileLoggingEnabled := false + if len(cfg.Logger.LogsDir) > 0 { + fileLoggingEnabled = true + } + if fileLoggingEnabled { + logsDir := cfg.Logger.LogsDir + if err := os.MkdirAll(logsDir, 0744); err != nil { + fmt.Printf("can't create log directory, so file logging is disabled, error: %s", err.Error()) + } else { + fileWriter := &lumberjack.Logger{ + Filename: path.Join(logsDir, "callout.log"), + MaxBackups: 3, // files + MaxSize: 10, // megabytes + MaxAge: 30, // days + } + + writers = append(writers, fileWriter) + //fmt.Printf("file logging is enabled, logsDir: %s\n", logsDir) + } + } + mw := io.MultiWriter(writers...) + + log.Logger = zerolog.New(mw).With().Timestamp().Logger() + //log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"}) + + switch strings.ToLower(cfg.Logger.LogLevel) { + case "panic": + zerolog.SetGlobalLevel(zerolog.PanicLevel) + case "fatal": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + case "error": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "warn": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "info": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "trace": + zerolog.SetGlobalLevel(zerolog.TraceLevel) + default: + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } +} diff --git a/samples/callout/go.mod b/samples/callout/go.mod new file mode 100644 index 0000000..5a6be47 --- /dev/null +++ b/samples/callout/go.mod @@ -0,0 +1,37 @@ +module github.com/reddydMSFT/callout + +go 1.21.1 + +require github.com/rs/zerolog v1.31.0 + +require ( + github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.3.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.10.0 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/text v0.13.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/gorilla/handlers v1.5.1 + github.com/gorilla/mux v1.8.0 + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/spf13/viper v1.17.0 + golang.org/x/sys v0.12.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 +) diff --git a/samples/callout/go.sum b/samples/callout/go.sum new file mode 100644 index 0000000..3e4eba4 --- /dev/null +++ b/samples/callout/go.sum @@ -0,0 +1,502 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= +github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= +github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/samples/callout/pkg/models/quality.go b/samples/callout/pkg/models/quality.go new file mode 100644 index 0000000..a5205e4 --- /dev/null +++ b/samples/callout/pkg/models/quality.go @@ -0,0 +1,25 @@ +package models + +type Quality struct { + Payload struct { + Age int `json:"age"` + AssetID string `json:"asset_id"` + AssetName string `json:"asset_name"` + Country string `json:"country"` + Humidity float64 `json:"humidity"` + ID string `json:"id"` + MachineStatus int `json:"machine_status"` + OperatingTime int `json:"operating_time"` + Pressure float64 `json:"pressure"` + Product string `json:"product"` + Site string `json:"site"` + Temperature float64 `json:"temperature"` + Vibration float64 `json:"vibration"` + QFactor float64 `json:"q_factor"` + Quality string `json:"quality"` + Shift int `json:"shift"` + SourceTimestamp string `json:"source_timestamp"` + } `json:"Payload"` + SequenceNumber int `json:"SequenceNumber"` + Timestamp string `json:"Timestamp"` +} diff --git a/samples/callout/pkg/serving/admin.go b/samples/callout/pkg/serving/admin.go new file mode 100644 index 0000000..2c220e2 --- /dev/null +++ b/samples/callout/pkg/serving/admin.go @@ -0,0 +1,39 @@ +package serving + +import ( + "fmt" + "net/http" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" +) + +func StartAdmin(port int) { + router := mux.NewRouter() + + // API routes + router.HandleFunc("/api/echo/{stage}", echoRequest).Methods(http.MethodGet) + router.HandleFunc("/api/echo/{stage}", echoRequest).Methods(http.MethodPost) + router.HandleFunc("/api/qfactor", qFactor).Methods(http.MethodPost) + + log.Info().Msgf("serving callout requests at http://localhost:%d/api", port) + log.Info().Msgf("you can configure callout stage with Get/POST to http://callout.default.svc.cluster.local/api/echo") + + // handle CORS + headersOK := handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}) + methodsOK := handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"}) + originsOK := handlers.AllowedOrigins([]string{"*"}) + + _ = http.ListenAndServe(fmt.Sprintf(":%d", port), handlers.CORS(headersOK, methodsOK, originsOK)(router)) +} + +// handleError log the error and return http error +func handleError(err error, w http.ResponseWriter) bool { + if err != nil { + log.Error().Err(err).Msg("error encountered while processing request") + http.Error(w, err.Error(), http.StatusInternalServerError) + return true + } + return false +} diff --git a/samples/callout/pkg/serving/echoRequest.go b/samples/callout/pkg/serving/echoRequest.go new file mode 100644 index 0000000..98515d3 --- /dev/null +++ b/samples/callout/pkg/serving/echoRequest.go @@ -0,0 +1,38 @@ +package serving + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" +) + +// echo the contents of the body back to response and print to STDOUT +func echoRequest(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + paramStr := "" + for key, value := range vars { + paramStr += key + "=" + value + " " + } + + req, err := io.ReadAll(r.Body) + if handleError(err, w) { + return + } + + // print pretty json + var prettyJSON bytes.Buffer + err = json.Indent(&prettyJSON, req, "", " ") + if handleError(err, w) { + return + } + + log.Debug().Msgf("Request: %s\n%s", paramStr, prettyJSON.String()) + + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(req) + handleError(err, w) +} diff --git a/samples/callout/pkg/serving/qFactor.go b/samples/callout/pkg/serving/qFactor.go new file mode 100644 index 0000000..997c80d --- /dev/null +++ b/samples/callout/pkg/serving/qFactor.go @@ -0,0 +1,60 @@ +package serving + +import ( + "encoding/json" + "io" + "net/http" + "time" + + "github.com/reddydMSFT/callout/pkg/models" +) + +// compute qfactor +func qFactor(w http.ResponseWriter, r *http.Request) { + req, err := io.ReadAll(r.Body) + if handleError(err, w) { + return + } + + var quality models.Quality + err = json.Unmarshal(req, &quality) + if handleError(err, w) { + return + } + + // compute qfactor + var compound = quality.Payload.Temperature * quality.Payload.Humidity + if quality.Payload.Age < 1 { + quality.Payload.QFactor = 1.0 + } else if compound > 7200 && compound <= 8000 { + quality.Payload.QFactor = 0.2 + } else if compound > 8000 && compound <= 9740 { + quality.Payload.QFactor = 0.8 + } else if compound > 9740 && compound <= 11000 { + quality.Payload.QFactor = 0.5 + } else { + quality.Payload.QFactor = 0.0 + } + + // set Quality name + if quality.Payload.QFactor >= 0.6 { + quality.Payload.Quality = "Good" + } else if quality.Payload.QFactor >= 0.3 && quality.Payload.QFactor < 0.6 { + quality.Payload.Quality = "Inspect" + } else { + quality.Payload.Quality = "Bad" + } + + // set Shift info + //aformat := time.RFC3339 + format := "2006-01-02T15:04:05.999Z" + ts, err := time.Parse(format, quality.Payload.SourceTimestamp) + if handleError(err, w) { + return + } + quality.Payload.Shift = (ts.Hour() / 8) + 1 + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(quality) + handleError(err, w) +} diff --git a/samples/callout/setup/service.yaml b/samples/callout/setup/service.yaml new file mode 100644 index 0000000..4747c92 --- /dev/null +++ b/samples/callout/setup/service.yaml @@ -0,0 +1,42 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: callout + namespace: default + labels: + app: callout +spec: + replicas: 1 + selector: + matchLabels: + app: callout + template: + metadata: + labels: + app: callout + spec: + containers: + - name: callout + image: azbluefin.azurecr.io/reddy-callout:stable + imagePullPolicy: Always + ports: + - name: http-8080 + containerPort: 8080 + protocol: TCP +--- +kind: Service +apiVersion: v1 +metadata: + name: callout + namespace: default + labels: + app: callout +spec: + ports: + - name: http-8080 + protocol: TCP + port: 80 + targetPort: 8080 + selector: + app: callout + type: ClusterIP