diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..ee1bfb2 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,2 @@ +build --define registry=nonexisting.example.com +build --define project=nonexisting diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..7ea4dc1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,6 @@ +Fixes #\ + +> It's a good idea to open an issue first for discussion. + +- [ ] Tests pass +- [ ] Appropriate changes to README are included in PR diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..81c661d --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,73 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: ci +on: [push, pull_request] +jobs: + build-test-bazel: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Cache bazel + uses: actions/cache@v2 + with: + path: | + ~/.cache/bazelisk + ~/.cache/bazel + key: bazel-${{ runner.os }}-${{ hashFiles('WORKSPACE', 'repositories.bzl') }} + - name: Check formating + run: bazel run @go_sdk//:bin/gofmt -- -d . && [ -z "$(bazel run --ui_event_filters=-DEBUG,-INFO --noshow_progress @go_sdk//:bin/gofmt -- -l .)" ] + - name: Check go mod synced + # go mod download needed because it's run from update-repos and can change go.sum because of https://github.com/golang/go/issues/45332. + run: bazel run @go_sdk//:bin/go mod tidy && bazel run @go_sdk//:bin/go mod download && [ -z "$(git status --porcelain)" ] + - name: Check go deps synced + run: bazel run //:gazelle -- update-repos -from_file=go.mod -to_macro=repositories.bzl%go_repositories -prune && [ -z "$(git status --porcelain)" ] + - name: Check gazelle synced + run: bazel run //:gazelle -- update -mode diff + - name: Check proto go code synced + run: ./scripts/regen_go_proto.sh -t + - name: Build + run: bazel build //... + - name: Test + run: bazel test //... + build-test-stdgo: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: '^1.16.0' + - name: Build + run: go build ./... + - name: Test + run: go test ./... -v + check-terraform: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + terraform_version: 0.15.x + - name: Format + run: terraform -chdir=infra fmt -check + - name: Init + run: terraform -chdir=infra init -backend=false + - name: Validate + run: terraform -chdir=infra validate -no-color diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b0fb09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/*.pem +/bazel-* +/config.toml diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..55e4129 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,29 @@ +load("@bazel_gazelle//:def.bzl", "gazelle") +load("@com_github_bazelbuild_buildtools//buildifier:def.bzl", "buildifier", "buildifier_test") +load("@io_bazel_rules_go//go:def.bzl", "nogo") + +# gazelle:prefix github.com/p2004a/gbcsdpd +gazelle(name = "gazelle") + +buildifier(name = "buildifier") + +buildifier( + name = "buildifier_check", + mode = "check", +) + +buildifier_test( + name = "buildifier_check_test", + srcs = ["WORKSPACE"] + glob([ + "**/*.bazel", + "**/*.bzl", + ]), + lint_mode = "warn", + mode = "check", +) + +nogo( + name = "my_nogo", + vet = True, + visibility = ["//visibility:public"], +) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e5c3b28 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement (CLA). You (or your employer) retain the copyright to your +contribution; this simply gives us permission to use and redistribute your +contributions as part of the project. Head over to + to see your current agreements on file or +to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code Reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google/conduct/). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..16a51a7 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# gbcsdpd + +![CI](https://github.com/p2004a/gbcsdpd/actions/workflows/ci.yaml/badge.svg) + +gbcsdpd is a simple Go Bluetooth Climate Sensor Data Publisher Daemon: it +listens for BLE advertisements containing measurements from Bluetooth sensors, +parses them, and publishes via [MQTT](https://mqtt.org/) protocol. + +Currently, it supports only [RuuviTag](https://ruuvi.com/ruuvitag/) sensors but +it should be easy to add support for more. + +This repository also contains +[instructions with Terraform configuration](infra/) for setting up a Google +Cloud project to gather and present measurements on a monitoring dashboard: + + + +Deamon targets Linux and depends on the high-level +[BlueZ D-Bus API](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/). + +## Documentation + +The documentation is placed near the code. + +Best start at the building, usage, and configuration of the daemon described in +[cmd/gbcsdpd/](cmd/gbcsdpd/). + +[infra/](infra/) contains documentation for setting up receiver and monitoring +dashboard on GCP. + +## Contributing + +See [`CONTRIBUTING.md`](CONTRIBUTING.md) for details. + +## License + +Apache 2.0; see [`LICENSE`](LICENSE) for details. + +## Disclaimer + +This is not an officially supported Google product. diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..0346412 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,89 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "io_bazel_rules_go", + sha256 = "69de5c704a05ff37862f7e0f5534d4f479418afc21806c887db544a316f3cb6b", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz", + "https://github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz", + ], +) + +http_archive( + name = "com_google_protobuf", + sha256 = "543cac2905c7f583dead64ed85dae726cfc75ace56c4fb74c148b151c2597035", + strip_prefix = "protobuf-3.17.0", + urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.17.0.zip"], +) + +http_archive( + name = "bazel_gazelle", + sha256 = "62ca106be173579c0a167deb23358fdfe71ffa1e4cfdddf5582af26520f1c66f", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz", + "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz", + ], +) + +# Needed by io_bazel_rules_docker +http_archive( + name = "rules_python", + sha256 = "778197e26c5fbeb07ac2a2c5ae405b30f6cb7ad1f5510ea6fdac03bded96cc6f", + url = "https://github.com/bazelbuild/rules_python/releases/download/0.2.0/rules_python-0.2.0.tar.gz", +) + +http_archive( + name = "io_bazel_rules_docker", + sha256 = "59d5b42ac315e7eadffa944e86e90c2990110a1c8075f1cd145f487e999d22b3", + strip_prefix = "rules_docker-0.17.0", + urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.17.0/rules_docker-v0.17.0.tar.gz"], +) + +http_archive( + name = "com_github_bazelbuild_buildtools", + sha256 = "932160d5694e688cb7a05ac38efba4b9a90470c75f39716d85fb1d2f95eec96d", + strip_prefix = "buildtools-4.0.1", + url = "https://github.com/bazelbuild/buildtools/archive/4.0.1.zip", +) + +load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") + +go_rules_dependencies() + +go_register_toolchains( + nogo = "@//:my_nogo", + version = "1.16.4", +) + +load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") + +protobuf_deps() + +load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") + +# Need to set it here as gazelle_dependencies is pulling in old version. +go_repository( + name = "com_github_pelletier_go_toml", + importpath = "github.com/pelletier/go-toml", + sum = "h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc=", + version = "v1.9.1", +) + +gazelle_dependencies() + +load("@io_bazel_rules_docker//repositories:repositories.bzl", container_repositories = "repositories") + +container_repositories() + +load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps") + +container_deps() + +load("@io_bazel_rules_docker//go:image.bzl", _go_image_repos = "repositories") + +_go_image_repos() + +load("//:repositories.bzl", "go_repositories") + +# gazelle:repository_macro repositories.bzl%go_repositories +go_repositories() diff --git a/api/BUILD.bazel b/api/BUILD.bazel new file mode 100644 index 0000000..7167967 --- /dev/null +++ b/api/BUILD.bazel @@ -0,0 +1,23 @@ +load("@rules_proto//proto:defs.bzl", "proto_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") + +go_library( + name = "go_default_library", + embed = [":api_go_proto"], + importpath = "github.com/p2004a/gbcsdpd/api", + visibility = ["//visibility:public"], +) + +proto_library( + name = "api_proto", + srcs = ["climate.proto"], + visibility = ["//visibility:public"], +) + +go_proto_library( + name = "api_go_proto", + importpath = "github.com/p2004a/gbcsdpd/api", + proto = ":api_proto", + visibility = ["//visibility:public"], +) diff --git a/api/climate.pb.go b/api/climate.pb.go new file mode 100644 index 0000000..ac9c367 --- /dev/null +++ b/api/climate.pb.go @@ -0,0 +1,254 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.17.0 +// source: api/climate.proto + +package gbcsdpd_api_v1 + +import ( + proto "github.com/golang/protobuf/proto" + 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) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type Measurement struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SensorMac string `protobuf:"bytes,1,opt,name=sensor_mac,json=sensorMac,proto3" json:"sensor_mac,omitempty"` + Temperature float32 `protobuf:"fixed32,10,opt,name=temperature,proto3" json:"temperature,omitempty"` + Humidity float32 `protobuf:"fixed32,11,opt,name=humidity,proto3" json:"humidity,omitempty"` + Pressure float32 `protobuf:"fixed32,12,opt,name=pressure,proto3" json:"pressure,omitempty"` + BatteryVoltage float32 `protobuf:"fixed32,20,opt,name=battery_voltage,json=batteryVoltage,proto3" json:"battery_voltage,omitempty"` +} + +func (x *Measurement) Reset() { + *x = Measurement{} + if protoimpl.UnsafeEnabled { + mi := &file_api_climate_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Measurement) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Measurement) ProtoMessage() {} + +func (x *Measurement) ProtoReflect() protoreflect.Message { + mi := &file_api_climate_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 Measurement.ProtoReflect.Descriptor instead. +func (*Measurement) Descriptor() ([]byte, []int) { + return file_api_climate_proto_rawDescGZIP(), []int{0} +} + +func (x *Measurement) GetSensorMac() string { + if x != nil { + return x.SensorMac + } + return "" +} + +func (x *Measurement) GetTemperature() float32 { + if x != nil { + return x.Temperature + } + return 0 +} + +func (x *Measurement) GetHumidity() float32 { + if x != nil { + return x.Humidity + } + return 0 +} + +func (x *Measurement) GetPressure() float32 { + if x != nil { + return x.Pressure + } + return 0 +} + +func (x *Measurement) GetBatteryVoltage() float32 { + if x != nil { + return x.BatteryVoltage + } + return 0 +} + +type MeasurementsPublication struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Measurements []*Measurement `protobuf:"bytes,1,rep,name=measurements,proto3" json:"measurements,omitempty"` +} + +func (x *MeasurementsPublication) Reset() { + *x = MeasurementsPublication{} + if protoimpl.UnsafeEnabled { + mi := &file_api_climate_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MeasurementsPublication) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MeasurementsPublication) ProtoMessage() {} + +func (x *MeasurementsPublication) ProtoReflect() protoreflect.Message { + mi := &file_api_climate_proto_msgTypes[1] + 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 MeasurementsPublication.ProtoReflect.Descriptor instead. +func (*MeasurementsPublication) Descriptor() ([]byte, []int) { + return file_api_climate_proto_rawDescGZIP(), []int{1} +} + +func (x *MeasurementsPublication) GetMeasurements() []*Measurement { + if x != nil { + return x.Measurements + } + return nil +} + +var File_api_climate_proto protoreflect.FileDescriptor + +var file_api_climate_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6c, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x67, 0x62, 0x63, 0x73, 0x64, 0x70, 0x64, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x31, 0x22, 0xaf, 0x01, 0x0a, 0x0b, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x5f, 0x6d, 0x61, + 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x4d, + 0x61, 0x63, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x02, 0x52, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x75, 0x6d, 0x69, 0x64, 0x69, 0x74, 0x79, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x02, 0x52, 0x08, 0x68, 0x75, 0x6d, 0x69, 0x64, 0x69, 0x74, 0x79, + 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x73, 0x73, 0x75, 0x72, 0x65, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x02, 0x52, 0x08, 0x70, 0x72, 0x65, 0x73, 0x73, 0x75, 0x72, 0x65, 0x12, 0x27, 0x0a, 0x0f, + 0x62, 0x61, 0x74, 0x74, 0x65, 0x72, 0x79, 0x5f, 0x76, 0x6f, 0x6c, 0x74, 0x61, 0x67, 0x65, 0x18, + 0x14, 0x20, 0x01, 0x28, 0x02, 0x52, 0x0e, 0x62, 0x61, 0x74, 0x74, 0x65, 0x72, 0x79, 0x56, 0x6f, + 0x6c, 0x74, 0x61, 0x67, 0x65, 0x22, 0x5a, 0x0a, 0x17, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x3f, 0x0a, 0x0c, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x62, 0x63, 0x73, 0x64, 0x70, 0x64, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x52, 0x0c, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_api_climate_proto_rawDescOnce sync.Once + file_api_climate_proto_rawDescData = file_api_climate_proto_rawDesc +) + +func file_api_climate_proto_rawDescGZIP() []byte { + file_api_climate_proto_rawDescOnce.Do(func() { + file_api_climate_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_climate_proto_rawDescData) + }) + return file_api_climate_proto_rawDescData +} + +var file_api_climate_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_api_climate_proto_goTypes = []interface{}{ + (*Measurement)(nil), // 0: gbcsdpd.api.v1.Measurement + (*MeasurementsPublication)(nil), // 1: gbcsdpd.api.v1.MeasurementsPublication +} +var file_api_climate_proto_depIdxs = []int32{ + 0, // 0: gbcsdpd.api.v1.MeasurementsPublication.measurements:type_name -> gbcsdpd.api.v1.Measurement + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_api_climate_proto_init() } +func file_api_climate_proto_init() { + if File_api_climate_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_api_climate_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Measurement); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_climate_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MeasurementsPublication); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_api_climate_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_api_climate_proto_goTypes, + DependencyIndexes: file_api_climate_proto_depIdxs, + MessageInfos: file_api_climate_proto_msgTypes, + }.Build() + File_api_climate_proto = out.File + file_api_climate_proto_rawDesc = nil + file_api_climate_proto_goTypes = nil + file_api_climate_proto_depIdxs = nil +} diff --git a/api/climate.proto b/api/climate.proto new file mode 100644 index 0000000..6778307 --- /dev/null +++ b/api/climate.proto @@ -0,0 +1,36 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package gbcsdpd.api.v1; + +message Measurement { + string sensor_mac = 1; + + // The float value below can be set to NaN to indicate + // that value is not available. + + // Climate + float temperature = 10; // C + float humidity = 11; // RH % + float pressure = 12; // hPa + + // Sensor information + float battery_voltage = 20; // V +} + +message MeasurementsPublication { + repeated Measurement measurements = 1; +} diff --git a/cmd/gbcsdpd/BUILD.bazel b/cmd/gbcsdpd/BUILD.bazel new file mode 100644 index 0000000..c3e826c --- /dev/null +++ b/cmd/gbcsdpd/BUILD.bazel @@ -0,0 +1,21 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "github.com/p2004a/gbcsdpd/cmd/gbcsdpd", + visibility = ["//visibility:private"], + deps = [ + "//api:go_default_library", + "//pkg/blelistener:go_default_library", + "//pkg/config:go_default_library", + "//pkg/ruuviparse:go_default_library", + "//pkg/sinks:go_default_library", + ], +) + +go_binary( + name = "gbcsdpd", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) diff --git a/cmd/gbcsdpd/README.md b/cmd/gbcsdpd/README.md new file mode 100644 index 0000000..0ad2529 --- /dev/null +++ b/cmd/gbcsdpd/README.md @@ -0,0 +1,108 @@ +# gbcsdpd Daemon + +gbcsdpd daemon is a single go binary with a single configuration file that +listens for sensors BLE Advertisements via BlueZ D-Bus API and publishes them to +configured sinks. MQTT sink is the main sink, but there is also useful for +debugging stdout sink. + +## Building + +This repository is using [Bazel](https://bazel.build/) but it also supports +building binaries using the standard `go build` command, so both: + +``` +bazel build //cmd/gbcsdpd +``` + +and + +``` +go build ./cmd/gbcsdpd +``` + +work. + +## Usage + +Run the binary and it should start printing measurements on standard output. + +```sh +$ ./gbcsdpd +[default-sink] 00:11:22:33:44:55 = 23.96°C, 43.17%, 963.69hPa, 2.38V +[default-sink] aa:bb:cc:dd:ee:ff = 24.55°C, 41.74%, 963.15hPa, 2.51V +[default-sink] 00:11:22:33:44:55 = 23.96°C, 43.17%, 963.69hPa, 2.39V +[default-sink] aa:bb:cc:dd:ee:ff = 24.55°C, 41.76%, 963.17hPa, 2.51V +``` + +`gbcsdpd` needs to be configured to be more useful. Specify path co +configuration file on `-config` flag. Configuration file is using +[TOML](https://toml.io/) format. + +The default configuration for the default behavior above looks like: + +```toml +adapter = "hci0" # this is the default value that you can omit + +[[sinks.stdout]] # this sink is only added when there aren't any other defined +name = "default-sink" +``` + +and you can start `gbcsdpd` and load it: `./gbcsdpd -config config.toml`. + +### Configuration + +The only top-level setting is the Bluetooth adapter name and the rest of the +configuration consists of a list of sinks to push publications to. There can be +multiple sinks of the same and different types in the same configuration. There +are currently 3 types of sinks implemented: + +- Stdout: useful for debugging, prints measurements on stdout. +- MQTT: generic MQTT target allowing to specify username, password, topic, + format, etc. +- GCP: MQTT sink which implements custom authorization scheme required by Cloud + IoT. Internally it's a simple wrapper over generic MQTT implementation. + +Data to MQTT servers is published as +[gbcsdpd.api.v1.MeasurementsPublication](../../api/climate.proto) Protobuf +messages serialized to JSON or binary format (`format` config option on MQTT +sink). + +The reference and documentation for all available configuration options is in +the [pkg/config/config_format.go](../../pkg/config/config_format.go) file. +`fConfig` type is the root of configuration. + +To see how does an example configuration with a lot of options set looks like +see +[pkg/config/testdata/test1/config.toml](../../pkg/config/testdata/test1/config.toml) +file. + +### Running as a service + +The [init/](../../init) directory in this repository contains instructions and +configuration templates for systemd and OpenWrt init systems. + +## Cross-compilation + +Both Bazel and standard go distribution support cross-compilation to different +architectures. + +### Bazel + +See documention at https://github.com/bazelbuild/rules_go#how-do-i-cross-compile + +For example arm+linux: + +``` +bazel build --platforms=@io_bazel_rules_go//go/toolchain:linux_arm //cmd/gbcsdpd +``` + +### Go Distribution + +See `$GOOS` and `$GOARCH` documentation at +https://golang.org/doc/install/source#environment. + +For example arm+linux: + +``` +GOOS=linux GOARCH=arm go build ./cmd/gbcsdpd +``` diff --git a/cmd/gbcsdpd/main.go b/cmd/gbcsdpd/main.go new file mode 100644 index 0000000..12ecd83 --- /dev/null +++ b/cmd/gbcsdpd/main.go @@ -0,0 +1,93 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "flag" + "log" + "math" + + api "github.com/p2004a/gbcsdpd/api" + "github.com/p2004a/gbcsdpd/pkg/blelistener" + "github.com/p2004a/gbcsdpd/pkg/config" + "github.com/p2004a/gbcsdpd/pkg/ruuviparse" + sinkspkg "github.com/p2004a/gbcsdpd/pkg/sinks" +) + +const ( + ruuviManufacturerID = 0x0499 +) + +func nilToNaN(value *float32) float32 { + if value == nil { + return float32(math.NaN()) + } + return *value +} + +func main() { + configPath := flag.String("config", "", "Path to the TOML config file") + logTime := flag.Bool("logtime", true, "If true log messages printed to stderr will contain time and date") + flag.Parse() + + if *logTime { + log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds) + } else { + log.SetFlags(0) + } + + conf, err := config.Read(*configPath) + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + var sinks []sinkspkg.Sink + for _, sinkConfig := range conf.Sinks { + sink, err := sinkspkg.NewSink(sinkConfig) + if err != nil { + log.Fatalf("Failed to create sink: %v", err) + } + sinks = append(sinks, sink) + } + + advListener, err := blelistener.NewAdvListener(conf.Adapter) + if err != nil { + log.Fatalf("Failed to listen for BLE advertisements: %v", err) + } + + for adv := range advListener.Advertisements() { + data, ok := adv.ManufacturerData[ruuviManufacturerID] + if !ok { + continue + } + ruuviData, err := ruuviparse.Parse(data) + if err != nil { + log.Printf("Failed to parse ruuvi data: %v", err) + } + measuement := &api.Measurement{ + SensorMac: adv.Address.String(), + Temperature: nilToNaN(ruuviData.Temperature), + Humidity: nilToNaN(ruuviData.Humidity), + Pressure: nilToNaN(ruuviData.Pressure), + BatteryVoltage: nilToNaN(ruuviData.BatteryVoltage), + } + for _, sink := range sinks { + sink.Publish(measuement) + } + } + if advListener.Err != nil { + log.Fatalf("BLE Advertisement listener failed: %v", advListener.Err) + } +} diff --git a/cmd/metricspusher/BUILD.bazel b/cmd/metricspusher/BUILD.bazel new file mode 100644 index 0000000..abd210a --- /dev/null +++ b/cmd/metricspusher/BUILD.bazel @@ -0,0 +1,53 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") +load("@io_bazel_rules_docker//go:image.bzl", "go_image") +load("@io_bazel_rules_docker//container:container.bzl", "container_push") + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "github.com/p2004a/gbcsdpd/cmd/metricspusher", + visibility = ["//visibility:private"], + deps = [ + "//api:go_default_library", + "@com_github_golang_protobuf//proto:go_default_library", + "@com_google_cloud_go//monitoring/apiv3:go_default_library", + "@go_googleapis//google/api:metric_go_proto", + "@go_googleapis//google/api:monitoredres_go_proto", + "@go_googleapis//google/monitoring/v3:monitoring_go_proto", + "@org_golang_google_grpc//codes:go_default_library", + "@org_golang_google_grpc//status:go_default_library", + "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", + ], +) + +go_binary( + name = "metricspusher", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) + +go_image( + name = "metricspusher_image", + binary = ":metricspusher", + visibility = ["//visibility:public"], +) + +container_push( + name = "push_metricspusher", + format = "Docker", + image = "metricspusher_image", + registry = "$(registry)", + repository = "$(project)/metricspusher", +) + +go_test( + name = "go_default_test", + srcs = ["main_test.go"], + embed = [":go_default_library"], + deps = [ + "//api:go_default_library", + "@com_github_golang_protobuf//proto:go_default_library", + "@com_github_google_go_cmp//cmp:go_default_library", + "@org_golang_google_protobuf//testing/protocmp:go_default_library", + ], +) diff --git a/cmd/metricspusher/README.md b/cmd/metricspusher/README.md new file mode 100644 index 0000000..b1004c3 --- /dev/null +++ b/cmd/metricspusher/README.md @@ -0,0 +1,10 @@ +# metricspusher + +This package provides an image for Cloud Run that receives Cloud Pub/Sub +messages containing serialized +[`gbcsdpd.api.v1.MeasurementsPublication`](../../api/climate.proto) and pushes +them to +[Cloud Monitoring Custom Metrics](https://cloud.google.com/monitoring/custom-metrics): +`custom.googleapis.com/sensor/measurement/{temperature,humidity,pressure,battery}`. + +See sources in [infra/](../../infra) for details about usage. diff --git a/cmd/metricspusher/main.go b/cmd/metricspusher/main.go new file mode 100644 index 0000000..de8a32a --- /dev/null +++ b/cmd/metricspusher/main.go @@ -0,0 +1,189 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "math" + "net/http" + "os" + "time" + + monitoring "cloud.google.com/go/monitoring/apiv3" + "github.com/golang/protobuf/proto" + gbcsdpdapipb "github.com/p2004a/gbcsdpd/api" + metricpb "google.golang.org/genproto/googleapis/api/metric" + monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres" + monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// https://cloud.google.com/pubsub/docs/reference/rpc/google.pubsub.v1#pubsubmessage +type inputPubSubMessage struct { + Message struct { + Data []byte `json:"data,omitempty"` + Attributes map[string]string `json:"attributes"` + MessageID string `json:"message_id"` + PublishTime time.Time `json:"publish_time"` + OrderingKey string `json:"ordering_key"` + } `json:"message"` + Subscription string `json:"subscription"` +} + +type measurementPubSubMessage struct { + DeviceID, DeviceRegistryLocaton, ProjectID string + PublishTime time.Time + Measurements []*gbcsdpdapipb.Measurement +} + +func parsePubsubMessage(data []byte) (*measurementPubSubMessage, error) { + var msg inputPubSubMessage + if err := json.Unmarshal(data, &msg); err != nil { + return nil, fmt.Errorf("json.Unmarshal: %v", err) + } + deviceID := msg.Message.Attributes["deviceId"] + deviceRegistryLocation := msg.Message.Attributes["deviceRegistryLocation"] + projectID := msg.Message.Attributes["projectId"] + subFolder := msg.Message.Attributes["subFolder"] + if deviceID == "" || deviceRegistryLocation == "" || projectID == "" { + return nil, fmt.Errorf("One of the required Attributes was not present: %v", msg.Message.Attributes) + } + if subFolder != "v1" { + return nil, fmt.Errorf("Only the v1 version of measurements is supported, got: %s", subFolder) + } + measuementPub := &gbcsdpdapipb.MeasurementsPublication{} + if err := proto.Unmarshal(msg.Message.Data, measuementPub); err != nil { + return nil, fmt.Errorf("Failed to unmarshal MeasurementsPublication from pubsub data: %v", err) + } + return &measurementPubSubMessage{ + DeviceID: deviceID, + DeviceRegistryLocaton: deviceRegistryLocation, + ProjectID: projectID, + PublishTime: msg.Message.PublishTime, + Measurements: measuementPub.Measurements, + }, nil +} + +func appendMeasurementTimeSeries( + ts []*monitoringpb.TimeSeries, + resource *monitoredrespb.MonitoredResource, + dimension string, t time.Time, value float32, +) []*monitoringpb.TimeSeries { + if math.IsNaN(float64(value)) { + return ts + } + return append(ts, &monitoringpb.TimeSeries{ + Metric: &metricpb.Metric{ + Type: "custom.googleapis.com/sensor/measurement/" + dimension, + }, + Resource: resource, + Points: []*monitoringpb.Point{ + { + Interval: &monitoringpb.TimeInterval{ + EndTime: timestamppb.New(t), + }, + Value: &monitoringpb.TypedValue{ + Value: &monitoringpb.TypedValue_DoubleValue{ + DoubleValue: float64(value), + }, + }, + }, + }, + }) +} + +func main() { + http.HandleFunc("/", handlePubSub) + port := os.Getenv("PORT") + if port == "" { + port = "8080" + log.Printf("Defaulting to port %s", port) + } + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatal(err) + } +} + +func handlePubSub(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + log.Printf("Got %s, not POST request for URL: %v", r.Method, r.URL) + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Printf("Body ioutil.ReadAll: %v", err) + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + psmsg, err := parsePubsubMessage(body) + if err != nil { + log.Printf("Failed to parse pubsub message: %v", err) + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + + // Creates a client. + client, err := monitoring.NewMetricClient(r.Context()) + if err != nil { + log.Printf("Failed to create monitoring client: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + defer client.Close() + + var ts []*monitoringpb.TimeSeries + for _, m := range psmsg.Measurements { + // https://cloud.google.com/monitoring/api/resources#tag_generic_node + res := &monitoredrespb.MonitoredResource{ + Type: "generic_node", + Labels: map[string]string{ + "project_id": psmsg.ProjectID, + "location": psmsg.DeviceRegistryLocaton, + "namespace": psmsg.DeviceID, + "node_id": m.SensorMac, + }, + } + ts = appendMeasurementTimeSeries(ts, res, "temperature", psmsg.PublishTime, m.Temperature) + ts = appendMeasurementTimeSeries(ts, res, "humidity", psmsg.PublishTime, m.Humidity) + ts = appendMeasurementTimeSeries(ts, res, "pressure", psmsg.PublishTime, m.Pressure) + ts = appendMeasurementTimeSeries(ts, res, "battery", psmsg.PublishTime, m.BatteryVoltage) + } + + // Writes time series data. + if err := client.CreateTimeSeries(r.Context(), &monitoringpb.CreateTimeSeriesRequest{ + Name: monitoring.MetricProjectPath(psmsg.ProjectID), + TimeSeries: ts, + }); err != nil { + // If we receive INVALID_ARGUMENT, it likely means that we pushed timeseries out-of-order + // because pubsub delivered them to us out-of-order. Let's ACK message with 202 in this case + // to prevent from retrying incorrect request. + if s, ok := status.FromError(err); ok { + if s.Code() == codes.InvalidArgument { + log.Printf("Got INVALID_ARGUMENT from CreateTimeSeries, assuming got data from pubsub out-of-order, full error: %v", err) + w.WriteHeader(http.StatusAccepted) + return + } + } + log.Printf("Failed to write time series data: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } +} diff --git a/cmd/metricspusher/main_test.go b/cmd/metricspusher/main_test.go new file mode 100644 index 0000000..e02584d --- /dev/null +++ b/cmd/metricspusher/main_test.go @@ -0,0 +1,87 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/base64" + "encoding/json" + "testing" + "time" + + "github.com/golang/protobuf/proto" + "github.com/google/go-cmp/cmp" + api "github.com/p2004a/gbcsdpd/api" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestParsePubsubMessage(t *testing.T) { + mpub := &api.MeasurementsPublication{ + Measurements: []*api.Measurement{ + { + SensorMac: "01:23:45:67:89:01", + Temperature: 20.0, + Humidity: 50.0, + Pressure: 1024.0, + BatteryVoltage: 3.0, + }, + { + SensorMac: "01:23:45:67:89:02", + Temperature: 22.0, + Humidity: 51.0, + Pressure: 1024.0, + BatteryVoltage: 3.0, + }, + }, + } + mpubSer, err := proto.Marshal(mpub) + if err != nil { + t.Fatalf("Failed to marshal proto data: %v", err) + } + pubsubmsgJSON, err := json.Marshal(map[string]interface{}{ + "message": map[string]interface{}{ + "attributes": map[string]string{ + "deviceId": "testing-device", + "deviceNumId": "2955148207840174", + "deviceRegistryId": "sensors", + "deviceRegistryLocation": "europe-west1", + "projectId": "some-project-123123", + "subFolder": "v1", + }, + "data": base64.StdEncoding.EncodeToString(mpubSer), + "messageId": "1650549360205915", + "message_id": "1650549360205915", + "publishTime": "2020-10-22T15:07:36.646Z", + "publish_time": "2020-10-22T15:07:36.646Z", + }, + "subscription": "projects/some-project-123123/subscriptions/measurements-subscription", + }) + if err != nil { + t.Fatalf("Failed to marshal json data: %v", err) + } + msg, err := parsePubsubMessage(pubsubmsgJSON) + if err != nil { + t.Fatalf("parsePubsubMessage failed: %v", err) + } + expectedMsg := &measurementPubSubMessage{ + DeviceID: "testing-device", + DeviceRegistryLocaton: "europe-west1", + ProjectID: "some-project-123123", + PublishTime: time.Date(2020, time.October, 22, 15, 7, 36, 646000000, time.UTC), + Measurements: mpub.Measurements, + } + if diff := cmp.Diff(msg, expectedMsg, protocmp.Transform()); diff != "" { + t.Errorf("unexpected difference:\n%v", diff) + } +} diff --git a/docs/dashboard-screenshot.png b/docs/dashboard-screenshot.png new file mode 100644 index 0000000..d59ee10 Binary files /dev/null and b/docs/dashboard-screenshot.png differ diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..c48464e --- /dev/null +++ b/docs/development.md @@ -0,0 +1,72 @@ +# Development hints + +Bag of things useful when working on the project. + +## Updating dependencies + +1. Manually go over repositories in `WORKSPACE` and update to the latest + versions. + +1. Update go deps with + + ```sh + bazel run @go_sdk//:bin/go -- get -u all + ``` + + followed by running mod tidy and updating the `repositories.bzl` using + gazelle update-repos. + + The most up-to-date commands to do that are in + [ci.yaml](../.github/workflows/ci.yaml) which verifies that all of those are + in sync. + + From time to time you might also want to manually go through the deps and + check whatever there are some new major versions released: this will require + changes to the code though. + +1. Verify that all builds and passes tests. Pay attention to any deprecation + warnings that might show up in the output because the interface of some rules + changed. + +## Debugging Cloud IoT integration + +This assumes environment setup as in [../infra/README.md](../infra/README.md). + +The steps below are useful for debugging some parts of Cloud IoT integration. +For example + +- See raw messages on pubsub topic as send by deamon +- Publish data to Cloud IoT and see how it shows up in pubsub + +1. Create keys and add a device: + + ```sh + openssl genpkey -algorithm RSA -out rsa_private.pem -pkeyopt rsa_keygen_bits:2048 + openssl rsa -in rsa_private.pem -pubout -out rsa_public.pem + gcloud --project=$PROJECT_ID iot devices create --region=$REGION \ + --registry=sensors --public-key path=rsa_public.pem,type=rsa-pem testing-device + ``` + +1. To create, pull and remove pubsub subscription: + + ```sh + gcloud --project=$PROJECT_ID pubsub subscriptions create --topic=measurements test-measurements-subscription + gcloud --project=$PROJECT_ID pubsub subscriptions pull --auto-ack --limit=10 test-measurements-subscription + gcloud --project=$PROJECT_ID pubsub subscriptions delete test-measurements-subscription + ``` + +1. To send a message we need to create JWTs, in Debian `python3-jwt` package + provides `pyjwt3` binary: + + ```sh + curl -X POST \ + -H "authorization: Bearer $(pyjwt3 \ + --key="$(cat rsa_private.pem)" \ + --alg=RS256 \ + encode aud=${PROJECT_ID} exp=+10 iat=$(date +%s) \ + )" \ + -H 'cache-control: no-cache' \ + -H 'content-type: application/json' \ + --data "{\"binary_data\": \"$(base64 <<< '{"message": "test"}')\"}" \ + "https://cloudiotdevice.googleapis.com/v1/projects/${PROJECT_ID}/locations/${REGION}/registries/sensors/devices/testing-device:publishEvent" + ``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d3e060a --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/p2004a/gbcsdpd + +go 1.16 + +require ( + cloud.google.com/go v0.81.0 + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/eclipse/paho.mqtt.golang v1.3.4 + github.com/fhmq/hmq v0.0.0-20210318020249-ccbe364f9fbe + github.com/godbus/dbus/v5 v5.0.4 + github.com/golang/protobuf v1.5.2 + github.com/google/go-cmp v0.5.5 + github.com/pelletier/go-toml v1.9.1 + golang.org/x/crypto v0.0.0-20210513122933-cd7d49e622d5 // indirect + golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect + golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 // indirect + google.golang.org/api v0.46.0 // indirect + google.golang.org/genproto v0.0.0-20210510173355-fb37daa5cd7a + google.golang.org/grpc v1.37.1 + google.golang.org/protobuf v1.26.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..906a794 --- /dev/null +++ b/go.sum @@ -0,0 +1,634 @@ +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.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.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +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 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= +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 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= +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 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= +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 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798 h1:2T/jmrHeTezcCM58lvEQXs0UpQJCo5SoGAcg+mbSTIg= +github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Shopify/sarama v1.23.0 h1:slvlbm7bxyp7sKQbUwha5BQdZTqurhRoI+zbKorVigQ= +github.com/Shopify/sarama v1.23.0/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs= +github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +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 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= +github.com/eclipse/paho.mqtt.golang v1.3.4 h1:/sS2PA+PgomTO1bfJSDJncox+U7X5Boa3AfhEywYdgI= +github.com/eclipse/paho.mqtt.golang v1.3.4/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= +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/go-control-plane v0.9.9-0.20210217033140-668b12f5399d h1:QyzYnTnPE15SQyUeqU6qLbWxMkwyAyu+vGksa0b7j00= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fhmq/hmq v0.0.0-20210318020249-ccbe364f9fbe h1:bLEsIK6oQ95esJ3uusPCiUXYI8qd2QtbdaQjJeGvpI0= +github.com/fhmq/hmq v0.0.0-20210318020249-ccbe364f9fbe/go.mod h1:8LeVIPKskGx+23MAOy9fQ8HWSK7uFLjviwtfu2idxmo= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= +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 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= +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 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +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 h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +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/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +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.3/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/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +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 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= +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-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5 h1:zIaiqGYDQwa4HVx5wGRTXbx38Pqxjemn4BP98wpzpXo= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +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 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03 h1:FUwcHNlEqkqLjLBdCp5PRlCFijNjvcYANOZXzCfXwCM= +github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +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 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml v1.9.1 h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc= +github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41 h1:GeinFsrjWz97fAxVUEd748aV0cYL+I6k44gFJTCVvpU= +github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ= +github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w= +github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= +github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= +github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= +github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +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 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= +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.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +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-20190701094942-4def268fd1a4/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-20210513122933-cd7d49e622d5 h1:N6Jp/LCiEoIBX56BZSR2bepK5GtbSC2DDOYT742mMfE= +golang.org/x/crypto v0.0.0-20210513122933-cd7d49e622d5/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +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 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= +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 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +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 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= +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 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= +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-20200425230154-ff2c4b7c35a0/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-20201110031124-69a78807bb2b/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-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/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/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c h1:SgVl/sCtkicsS7psKkje4H9YtjdEl3xsYh7N+5TDHqY= +golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/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/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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-20190222072716-a9d3bda3a223/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-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-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-20190730183949-1393eb018365/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-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 h1:yhBbb4IRs2HS9PPlAg6DMC6mUOKexJBNsLf4Z+6En1Q= +golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +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.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +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/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.46.0 h1:jkDWHOBIoNSD0OQpq4rtBVu+Rh325MPjXG1rakAp8JU= +google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I= +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 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +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-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210510173355-fb37daa5cd7a h1:tzkHckzMzgPr8SC4taTC3AldLr4+oJivSoq1xf/nhsc= +google.golang.org/genproto v0.0.0-20210510173355-fb37daa5cd7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +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/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1 h1:ARnQJNWxGyYJpdf/JXscNlQr/uv607ZPU9Z7ogHi+iI= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +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= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw= +gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= +gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM= +gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= +gopkg.in/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI= +gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= +gopkg.in/jcmturner/gokrb5.v7 v7.2.3 h1:hHMV/yKPwMnJhPuPx7pH2Uw/3Qyf+thJYlisUc44010= +gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= +gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU= +gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-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 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/infra/.gitignore b/infra/.gitignore new file mode 100644 index 0000000..815ee1b --- /dev/null +++ b/infra/.gitignore @@ -0,0 +1,5 @@ +.terraform/ +.terraform.lock.hcl +terraform.tfvars +creds.json +*.pem diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..ad77933 --- /dev/null +++ b/infra/README.md @@ -0,0 +1,216 @@ +# Infrastructure + +This describes the steps to set up and configure a GCP project that hosts a +receiver of the measurements published by a gbcsdpd instance and a dashboard to +view data. + +We use [Terraform](https://www.terraform.io/) to configure resources in the +project but there are still some manual steps required because the created GCP +project is self-contained. We store +[Terraform state](https://www.terraform.io/docs/language/state/index.html) in +the GCS bucket in the project itself, and are not using for example Terraform +Admin Project pattern as described in +[Managing Google Cloud projects with Terraform](https://cloud.google.com/community/tutorials/managing-gcp-projects-with-terraform) +thus we need to create and prepare the project for Terraform manually. + +## One time setup + +These one-time setup instructions need to be executed only once and the +[Maintenance](#maintanance) section describes how to make changes to it. + +### Preparation + +Install and configure: + +- [Bazel](https://docs.bazel.build/versions/master/install.html) +- [gcloud](https://cloud.google.com/sdk/gcloud) +- [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli) +- [openssl cli](https://wiki.openssl.org/index.php/Command_Line_Utilities) + +### Setup GCP Project + +1. Source `setup.sh` into shell environment. This script sets up `$PROJECT_ID`, + `$REGION`, `$CONTAINER_REGISTRY` environment variables and creates + `terraform.tfvars` file. + + ```sh + $ source setup.sh + project id: climate-station-273399 + gcp region: europe-west1 + container registry: eu.gcr.io + ``` + + To restore environment variables after eg. restarting shell just source + `setup.sh` again and it will use values from existing `terraform.tfvars`. + +1. Create a project + + ```sh + gcloud projects create $PROJECT_ID --name="Climate Station" + ``` + + If you want to create the project in an organization add + `--organization=ORG_ID` to the command above. + +1. Link a billing account to the project + + ```sh + gcloud beta billing accounts list + gcloud beta billing projects link --billing-account=XXXXXX-XXXXXX-XXXXXX $PROJECT_ID + ``` + +1. Enable the Resource Manager API and Container Registry API + + ```sh + gcloud --project=$PROJECT_ID services enable cloudresourcemanager.googleapis.com containerregistry.googleapis.com + ``` + +1. Create docker authentication settings for pushing images to the container + registry + + ```sh + gcloud auth configure-docker + ``` + + Bazel `container_push` rule will use it in the next step. + +1. Build and push a [`metricspusher`](../cmd/metricspusher) image to the + container registry: + + ```sh + bazel run --define project=$PROJECT_ID --define registry=$CONTAINER_REGISTRY \ + --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 \ + //cmd/metricspusher:push_metricspusher + ``` + +1. Create a service account for Terraform, get a key (saved in `creds.json`) and + add it to the project's IAM policy: + + ```sh + gcloud --project=$PROJECT_ID iam service-accounts create terraform + gcloud --project=$PROJECT_ID iam service-accounts keys create creds.json \ + --iam-account=terraform@${PROJECT_ID}.iam.gserviceaccount.com + gcloud projects add-iam-policy-binding $PROJECT_ID --role=roles/owner \ + --member=serviceAccount:terraform@${PROJECT_ID}.iam.gserviceaccount.com + ``` + +1. Create a bucket for Terraform state: + + ```sh + gsutil mb -p $PROJECT_ID -c STANDARD -l $REGION -b on gs://tfstate-${PROJECT_ID}/ + ``` + +1. Intialize Terraform: + + ```sh + terraform init -backend-config="bucket=tfstate-${PROJECT_ID}" + ``` + +1. Import the Terraform state GCS bucket resource into the Terraform state: + + ```sh + terraform import google_storage_bucket.tfstate_bucket tfstate-${PROJECT_ID} + ``` + +1. Now, after we've finally set up the project and Terraform we can use it to + set up all other GCP resources: + + ```sh + terraform apply + ``` + + It sometimes happened to me that this command failed because some resources + were not ready. Just retry. + +### Setup publishing daemon + +1. Name our daemon instance somehow, eg. `climate-publisher` or pick the + hostname of the device it's running on. + + ```sh + DEVICE_NAME=climate-publisher + ``` + +1. Create a key pair for authentication of the publisher with Cloud IoT. + + ```sh + openssl genpkey -algorithm RSA -out rsa_private.pem -pkeyopt rsa_keygen_bits:2048 + openssl rsa -in rsa_private.pem -pubout -out rsa_public.pem + ``` + +1. Add the publisher as a new device to our Cloud IoT registry. + + ```sh + gcloud --project=$PROJECT_ID iot devices create --region=$REGION --registry=sensors \ + --public-key path=rsa_public.pem,type=rsa-pem $DEVICE_NAME + ``` + +1. Append following GCP sink configuration to your `config.toml` daemon + configuration (see [cmd/gbcsdpd](cmd/gbcsdpd) for details about daemon + setup). + + ```sh + cat <> config.toml + [[sinks.gcp]] + project = "$PROJECT_ID" + region = "$REGION" + registry = "sensors" + device = "$DEVICE_NAME" + key = "rsa_private.pem" + rate_limit.max_1_in = "2m" + EOF + ``` + + Don't forget to keep the generated `rsa_private.pem` next to the + `config.toml` file. + +### Finish configuring monitoring dashboard + +Terraform also created a "Climate Station" Cloud Monitoring dashboard for all +the measurements that you can find at +https://console.cloud.google.com/monitoring/dashboards/. It will populate after +deamon starts publishing data. + +You can notice that the lines on the graph don't have any friendly names, but +MAC adresses of devices publishing data. To set some friendly names on the +dashboard: + +1. Add `sensors` variable to the `terraform.tfvars` that maps MAC to friendly + name, eg: + + ``` + sensors = { + "aa:bb:cc:ee:dd:ee" = "balcony" + "11:33:55:11:55:11" = "living room" + "42:66:66:99:00:ff" = "bedroom" + } + ``` + +1. Run `terraform apply` to update the monitoring dashboard. + +## Maintenance + +This doesn't require any ongoing maintenace, so this section is only useful for +doing upgrades of infrastructure or `metricpusher` image. + +### Re-setup on a different machine + +Once the one-time setup above is done and we would like to make some changes +with Terraform from a different machine, we needs to only: + +1. Run `setup.sh` providing known values (or copy `terraform.tfvars` to not have + to retype those) to set environment variables. +1. Get `terraform@${PROJECT_ID}.iam.gserviceaccount.com` key into `creds.json`. +1. Run `terraform init -backend-config="bucket=tfstate-${PROJECT_ID}"` to + initialize Terraform. +1. Terraform is ready for any plan/apply commands. + +### Updating the Cloud Run services + +Push the new image to the container registry using the Bazel command from setup +step 6, and then update Cloud Run service to the new latest image with: + +```sh +gcloud run deploy metricspusher --platform managed --region $REGION \ + --image $CONTAINER_REGISTRY/$PROJECT_ID/metricspusher:latest +``` diff --git a/infra/backend.tf b/infra/backend.tf new file mode 100644 index 0000000..af7842b --- /dev/null +++ b/infra/backend.tf @@ -0,0 +1,21 @@ +/** + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +terraform { + backend "gcs" { + credentials = "creds.json" + } +} \ No newline at end of file diff --git a/infra/main.tf b/infra/main.tf new file mode 100644 index 0000000..2962bbe --- /dev/null +++ b/infra/main.tf @@ -0,0 +1,236 @@ +/** + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +terraform { + required_version = ">= 0.15" + required_providers { + google = { + source = "hashicorp/google" + version = "~> 3.66.1" + } + } +} + +provider "google" { + credentials = var.creds + project = var.project + region = var.region +} + +data "google_project" "project" { +} + +resource "google_storage_bucket" "tfstate_bucket" { + name = "tfstate-${var.project}" + location = var.region + storage_class = "STANDARD" + uniform_bucket_level_access = true + + versioning { + enabled = true + } + lifecycle_rule { + condition { + age = "14" + with_state = "ARCHIVED" + } + action { + type = "Delete" + } + } +} + +resource "google_project_service" "service" { + for_each = toset(["pubsub", "cloudiot", "monitoring", "iam", "run"]) + service = "${each.value}.googleapis.com" +} + +resource "google_pubsub_topic" "measurements_topic" { + name = "measurements" + depends_on = [google_project_service.service["pubsub"]] +} + +resource "google_cloudiot_registry" "sensors_registry" { + name = "sensors" + + event_notification_configs { + pubsub_topic_name = google_pubsub_topic.measurements_topic.id + subfolder_matches = "" + } + + mqtt_config = { + mqtt_enabled_state = "MQTT_ENABLED" + } + + http_config = { + http_enabled_state = "HTTP_ENABLED" + } + + depends_on = [google_project_service.service["cloudiot"]] +} + +locals { + metrics = [ + { name = "temperature", unit = "{C}", pretty = "Temperature" }, + { name = "humidity", unit = "%%{RH}", pretty = "Humidity" }, + { name = "pressure", unit = "{hPa}", pretty = "Pressure" }, + { name = "battery", unit = "{V}", pretty = "Battery voltage" }, + ] +} + +resource "google_monitoring_metric_descriptor" "metric" { + for_each = { for metric in local.metrics : metric.name => metric } + + description = "Sensor measurement of ${each.key}" + display_name = title(each.key) + type = "custom.googleapis.com/sensor/measurement/${each.key}" + metric_kind = "GAUGE" + value_type = "DOUBLE" + unit = each.value.unit + + depends_on = [google_project_service.service["monitoring"]] +} + +locals { + mac_renamer = length(var.sensors) == 0 ? "" : "\n| map [sensor: ${join("", [for mac, name in var.sensors : "if(sensor == '${mac}', '${name}', "])}sensor${join("", [for _ in var.sensors : ")"])}]" +} + +resource "google_monitoring_dashboard" "climate-station-dashboard" { + dashboard_json = jsonencode({ + "displayName" : "Climate Station", + "gridLayout" : { + "columns" : "2", + "widgets" : [for metric in local.metrics : { + "title" : metric.pretty, + "xyChart" : { + "chartOptions" : { + "mode" : "COLOR" + }, + "dataSets" : [ + { + "plotType" : "LINE", + "timeSeriesQuery" : { + "timeSeriesQueryLanguage" : "fetch generic_node\n| metric 'custom.googleapis.com/sensor/measurement/${metric.name}'\n| map rename[sensor: resource.node_id]${local.mac_renamer}" + } + } + ], + "timeshiftDuration" : "0s", + "yAxis" : { + "label" : "y1Axis", + "scale" : "LINEAR" + } + } + }] + } + }) + + depends_on = [google_project_service.service["monitoring"], google_monitoring_metric_descriptor.metric] +} + +resource "google_service_account" "metricspusher-invoker" { + account_id = "metricspusher-invoker" + display_name = "metricspusher invoker" + description = "Service account allowed to invoke the metricspusher Cloud Run service" + + depends_on = [google_project_service.service["iam"], google_project_service.service["pubsub"]] +} + +data "google_iam_policy" "metricspusher-invoker-policy" { + binding { + role = "roles/iam.serviceAccountTokenCreator" + members = ["serviceAccount:service-${data.google_project.project.number}@gcp-sa-pubsub.iam.gserviceaccount.com"] + } +} + +resource "google_service_account_iam_policy" "metricspusher-invoker-iam" { + service_account_id = google_service_account.metricspusher-invoker.name + policy_data = data.google_iam_policy.metricspusher-invoker-policy.policy_data +} + +resource "google_service_account" "metricspusher" { + account_id = "metricspusher" + display_name = "metricspusher" + description = "Service account running the metricspusher Cloud Run service" + + depends_on = [google_project_service.service["iam"]] +} + +resource "google_project_iam_member" "metricspusher-metrics-writer" { + role = "roles/monitoring.metricWriter" + member = "serviceAccount:${google_service_account.metricspusher.email}" +} + +resource "google_cloud_run_service" "metricspusher-service" { + name = "metricspusher" + location = var.region + + template { + spec { + containers { + image = "${var.container_registry}/${var.project}/metricspusher:latest" + } + service_account_name = google_service_account.metricspusher.email + } + } + + traffic { + percent = 100 + latest_revision = true + } + + autogenerate_revision_name = true + + depends_on = [google_project_service.service["run"]] +} + +data "google_iam_policy" "metricspusher-service-policy" { + binding { + role = "roles/run.invoker" + members = ["serviceAccount:${google_service_account.metricspusher-invoker.email}"] + } +} + +resource "google_cloud_run_service_iam_policy" "metricspusher-service-iam" { + location = google_cloud_run_service.metricspusher-service.location + project = google_cloud_run_service.metricspusher-service.project + service = google_cloud_run_service.metricspusher-service.name + + policy_data = data.google_iam_policy.metricspusher-service-policy.policy_data +} + +resource "google_pubsub_subscription" "measurements-subscription" { + name = "measurements-subscription" + topic = google_pubsub_topic.measurements_topic.name + + ack_deadline_seconds = 20 + message_retention_duration = "3600s" + + push_config { + push_endpoint = google_cloud_run_service.metricspusher-service.status[0].url + oidc_token { + service_account_email = google_service_account.metricspusher-invoker.email + } + # Currently there is only one version and keeping it causes permament diff. + # attributes = { + # x-goog-version = "v1" + # } + } + + depends_on = [ + google_service_account_iam_policy.metricspusher-invoker-iam, + google_cloud_run_service_iam_policy.metricspusher-service-iam, + ] +} diff --git a/infra/setup.sh b/infra/setup.sh new file mode 100644 index 0000000..b04f805 --- /dev/null +++ b/infra/setup.sh @@ -0,0 +1,23 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [ ! -f "terraform.tfvars" ]; then + cat << EOF > terraform.tfvars +project = "$(bash -c "read -e -p 'project id: ' -i 'climate-station-$(printf "%06d" $(shuf -i 0-999999 -n 1))' R; echo \$R")" +region = "$(bash -c 'read -e -p "gcp region: " -i "europe-west1" R; echo $R')" +container_registry = "$(bash -c 'read -e -p "container registry: " -i "eu.gcr.io" R; echo $R')" +creds = "creds.json" +EOF +fi +source <(awk '{ m[$1] = $3 } END { print "PROJECT_ID=" m["project"]; print "REGION=" m["region"]; print "CONTAINER_REGISTRY=" m["container_registry"] }' terraform.tfvars) diff --git a/infra/variables.tf b/infra/variables.tf new file mode 100644 index 0000000..c3e42d0 --- /dev/null +++ b/infra/variables.tf @@ -0,0 +1,41 @@ +/** + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "project" { + description = "The GCP project id" + type = string +} + +variable "region" { + description = "GCP region to use for resources" + type = string +} + +variable "container_registry" { + description = "Name of the container registry where images are stored" + type = string +} + +variable "creds" { + description = "Path to the file with terraform service account credentials" + type = string +} + +variable "sensors" { + description = "Mapping from sensor MAC to friendly name for monitoring" + type = map(string) + default = {} +} diff --git a/init/openwrt/README.md b/init/openwrt/README.md new file mode 100644 index 0000000..10a2cbf --- /dev/null +++ b/init/openwrt/README.md @@ -0,0 +1,26 @@ +# OpenWrt init script + +1. Make sure you have `bluez-daemon` package installed on the device. You might + want to set `AutoEnable` to true in `/etc/bluetooth/main.conf` as it's off by + default. + +1. Optional: Install `ca-bundle` and `ca-certificates` packages if you don't use + TLS with self-signed certificates on the MQTT server and don't want to + provide root certificates chain yourself. + +1. Build for correct architecture as described in + [cmd/gbcsdpd](../../cmd/gbcsdpd). For example: + + ```sh + bazel build --platforms=@io_bazel_rules_go//go/toolchain:linux_arm //cmd/gbcsdpd + ``` + +1. Copy the binary to `/usr/bin` on the OpenWrt router. + +1. Create `/etc/gbcsdpd/config.toml` configuration. + +1. Copy the `gbcsdpd` init service file in this directory to `/etc/init.d/`. + +1. Enable service `service gbcsdpd enable`. + +1. Start service `service gbcsdpd start`. diff --git a/init/openwrt/gbcsdpd b/init/openwrt/gbcsdpd new file mode 100755 index 0000000..9905e37 --- /dev/null +++ b/init/openwrt/gbcsdpd @@ -0,0 +1,15 @@ +#!/bin/sh /etc/rc.common + +# start after bluetoothd (62) +START=64 +USE_PROCD=1 +PROG=/usr/bin/gbcsdpd + +start_service() { + procd_open_instance + procd_set_param command "$PROG" -config /etc/gbcsdpd/config.toml -logtime false + procd_set_param file /etc/gbcsdpd/config.toml + procd_set_param stderr 1 + procd_set_param pidfile /var/run/gbcsdpd.pid + procd_close_instance +} diff --git a/init/systemd/README.md b/init/systemd/README.md new file mode 100644 index 0000000..9402f9c --- /dev/null +++ b/init/systemd/README.md @@ -0,0 +1,7 @@ +# systemd init + +1. Put the `gbcsdpd` binary in `/usr/local/bin` +1. Create a `/usr/local/etc/gbcsdpd/config.toml` configuration file +1. Copy `gbcsdpd.service` to `/usr/local/lib/systemd/system` +1. Enable service `systemctl enable gbcsdpd.service` +1. Start service `systemctl start gbcsdpd.service` diff --git a/init/systemd/gbcsdpd.service b/init/systemd/gbcsdpd.service new file mode 100644 index 0000000..ab59cf5 --- /dev/null +++ b/init/systemd/gbcsdpd.service @@ -0,0 +1,12 @@ +[Unit] +Description=gbcsdpd Daemon +After=network-online.target +Requires=network-online.target +After=bluetooth.target +Requires=bluetooth.target + +[Service] +ExecStart=/usr/local/bin/gbcsdpd -config /usr/local/etc/gbcsdpd/config.toml -logtime false + +[Install] +WantedBy=multi-user.target diff --git a/pkg/backoff/BUILD.bazel b/pkg/backoff/BUILD.bazel new file mode 100644 index 0000000..306e371 --- /dev/null +++ b/pkg/backoff/BUILD.bazel @@ -0,0 +1,8 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["backoff.go"], + importpath = "github.com/p2004a/gbcsdpd/pkg/backoff", + visibility = ["//visibility:public"], +) diff --git a/pkg/backoff/backoff.go b/pkg/backoff/backoff.go new file mode 100644 index 0000000..de3ff90 --- /dev/null +++ b/pkg/backoff/backoff.go @@ -0,0 +1,31 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package backoff + +import ( + "math" + "math/rand" + "time" +) + +// Exponential computes how much time to wait before retrying. +func Exponential(retryNum int, baseDelay time.Duration, maxDelay time.Duration, factor float64) time.Duration { + if retryNum == 0 { + return 0 + } + backoff := float64(baseDelay) * math.Pow(factor, float64(retryNum)) + backoff -= backoff * 0.5 * rand.Float64() // add jitter + return time.Duration(math.Min(float64(maxDelay), backoff)) +} diff --git a/pkg/blelistener/BUILD.bazel b/pkg/blelistener/BUILD.bazel new file mode 100644 index 0000000..c0f8417 --- /dev/null +++ b/pkg/blelistener/BUILD.bazel @@ -0,0 +1,12 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["blelistener.go"], + importpath = "github.com/p2004a/gbcsdpd/pkg/blelistener", + visibility = ["//visibility:public"], + deps = [ + "//pkg/backoff:go_default_library", + "@com_github_godbus_dbus_v5//:go_default_library", + ], +) diff --git a/pkg/blelistener/blelistener.go b/pkg/blelistener/blelistener.go new file mode 100644 index 0000000..21efd66 --- /dev/null +++ b/pkg/blelistener/blelistener.go @@ -0,0 +1,369 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package blelistener implements AdvListener which listens for BLE +// advertisements and published them as Advertisement objects via +// channel returned by Advertisements() +// +// Currently this package does it by using org.bluez.Adapter1 interface: +// Starts discovery and listens for changes to ManufacturerData and RSSI +// properties of all org.bluez.Device1 objects under adapter that are +// propagated via org.freedesktop.DBus.Properties.PropertiesChanged signal. +// See https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc for Bluez +// D-Bus API documentation. +// +// In the future this package might switch to org.bluez.AdvertisementMonitor1 +// API but it's currently experimental and not available in stable builds. +package blelistener + +import ( + "fmt" + "log" + "net" + "sync" + "time" + + "github.com/godbus/dbus/v5" + "github.com/p2004a/gbcsdpd/pkg/backoff" +) + +type objectProperties map[string]dbus.Variant + +// org.freedesktop.DBus.Properties.PropertiesChanged signal +// see https://dbus.freedesktop.org/doc/dbus-specification.html +type propertiesChangedSignal struct { + InterfaceName string + ChangedProperties objectProperties + InvalidatedProperties []string +} + +// org.freedesktop.DBus.ObjectManager.InterfacesRemoved signal +// see https://dbus.freedesktop.org/doc/dbus-specification.html +type interfacesRemovedSignal struct { + ObjectPath dbus.ObjectPath + Interfaces []string +} + +// org.freedesktop.DBus.ObjectManager.InterfacesAdded signal +// see https://dbus.freedesktop.org/doc/dbus-specification.html +type interfacesAddedSignal struct { + ObjectPath dbus.ObjectPath + InterfacesAndProperties map[string]objectProperties +} + +// org.bluez.Device1.ManufacturerData property +// see https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt +type manufacturerDataProperty map[uint16]dbus.Variant + +// ManufacturerData represents a map from manufacturer ID to raw manufacturer +// data embeded in a BLE advertisement. +type ManufacturerData map[uint16][]byte + +// Advertisement represents a BLE advertisement. +type Advertisement struct { + Address net.HardwareAddr + ManufacturerData ManufacturerData +} + +func parseDeviceMAC(v dbus.Variant) (net.HardwareAddr, error) { + var strAddr string + err := v.Store(&strAddr) + if err != nil { + return nil, fmt.Errorf("failed to store address variant to string: %v", err) + } + addr, err := net.ParseMAC(strAddr) + if err != nil { + return nil, fmt.Errorf("address didn't contain valid MAC: %v", err) + } + return addr, nil +} + +func parseManufacturerData(v dbus.Variant) (ManufacturerData, error) { + var md manufacturerDataProperty + if err := v.Store(&md); err != nil { + return nil, fmt.Errorf("given data is not a org.bluez.Device1.ManufacturerData: %v", err) + } + res := make(ManufacturerData) + for k, v := range md { + var data []byte + if err := v.Store(&data); err != nil { + return nil, fmt.Errorf("failed to store bytes: %v", err) + } + res[k] = data + } + return res, nil +} + +func parseAdvertisementFromProperties(props objectProperties) (Advertisement, error) { + var adv Advertisement + + // Get Address + addrVariant, ok := props["Address"] + if !ok { + return adv, fmt.Errorf("org.bluez.Device1 doesn't have Address property") + } + addr, err := parseDeviceMAC(addrVariant) + if err != nil { + return adv, nil + } + adv.Address = addr + + // Get ManufacturerData + mdVariant, ok := props["ManufacturerData"] + if ok { + md, err := parseManufacturerData(mdVariant) + if err != nil { + return adv, fmt.Errorf("failed to parse manufacturer data: %v", err) + } + adv.ManufacturerData = md + } else { + adv.ManufacturerData = make(ManufacturerData) + } + return adv, nil +} + +// AdvListener uses DBUS Bluez interface to listen for BLE advertisements and +// returns them via Advertisements() channel. When the Advertisements() channel +// is closed, the Err field contains the error. +type AdvListener struct { + adapter dbus.BusObject + conn *dbus.Conn + m sync.Mutex // Guards advCache and Err + advCache map[dbus.ObjectPath]Advertisement + signals chan *dbus.Signal + results chan Advertisement + Err error +} + +// Advertisements returns a channel that AdvListener publishes advertisements on. +func (l *AdvListener) Advertisements() <-chan Advertisement { + return l.results +} + +func (l *AdvListener) publishAdvertisement(objPath dbus.ObjectPath, adv Advertisement) { + l.m.Lock() + defer l.m.Unlock() + l.advCache[objPath] = adv + if len(adv.ManufacturerData) > 0 { + l.results <- adv + } +} + +func (l *AdvListener) setError(err error) { + l.m.Lock() + defer l.m.Unlock() + if l.Err == nil { + l.Err = err + } + if err := l.conn.Close(); err != nil { + log.Printf("Closing system bus connection failed: %v", err) + } +} + +func (l *AdvListener) handlePropertiesChanged(objPath dbus.ObjectPath, changed *propertiesChangedSignal) error { + if changed.InterfaceName != "org.bluez.Device1" { + return nil + } + publish := false + l.m.Lock() + adv, ok := l.advCache[objPath] + l.m.Unlock() + if !ok { + var props objectProperties + if err := l.conn.Object("org.bluez", objPath).Call("org.freedesktop.DBus.Properties.GetAll", 0, "org.bluez.Device1").Store(&props); err != nil { + return fmt.Errorf("failed to get all properties of %s: %v", objPath, err) + } + var err error + adv, err = parseAdvertisementFromProperties(props) + if err != nil { + return fmt.Errorf("failed parse device properties into Advertisement: %v", err) + } + publish = true + } + + if v, ok := changed.ChangedProperties["ManufacturerData"]; ok { + md, err := parseManufacturerData(v) + if err != nil { + return fmt.Errorf("failed to parse manufacturer data: %v", err) + } + adv.ManufacturerData = md + publish = true + } + + if _, ok := changed.ChangedProperties["RSSI"]; ok { + publish = true + } + + if publish { + l.publishAdvertisement(objPath, adv) + } + return nil +} + +func (l *AdvListener) handleInterfacesAdded(added *interfacesAddedSignal) error { + deviceProps, ok := added.InterfacesAndProperties["org.bluez.Device1"] + if !ok { + return nil + } + adv, err := parseAdvertisementFromProperties(deviceProps) + if err != nil { + return fmt.Errorf("failed parse device properties into Advertisement: %v", err) + } + l.publishAdvertisement(added.ObjectPath, adv) + return nil +} + +func (l *AdvListener) signalHandlerLoop() { + defer close(l.results) + + if err := l.conn.AddMatchSignal( + dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), + dbus.WithMatchMember("PropertiesChanged"), + dbus.WithMatchPathNamespace(l.adapter.Path())); err != nil { + l.setError(fmt.Errorf("failed to add matcher for PropertiesChanged signal: %v", err)) + return + } + if err := l.conn.AddMatchSignal( + dbus.WithMatchInterface("org.freedesktop.DBus.ObjectManager"), + dbus.WithMatchMember("InterfacesRemoved")); err != nil { + l.setError(fmt.Errorf("failed to add matcher for InterfacesRemoved signal: %v", err)) + return + } + if err := l.conn.AddMatchSignal( + dbus.WithMatchInterface("org.freedesktop.DBus.ObjectManager"), + dbus.WithMatchMember("InterfacesAdded")); err != nil { + l.setError(fmt.Errorf("failed to add matcher for InterfacesAdded signal: %v", err)) + return + } + + for signal := range l.signals { + switch signal.Name { + case "org.freedesktop.DBus.Properties.PropertiesChanged": + var changed propertiesChangedSignal + if err := dbus.Store( + signal.Body, + &changed.InterfaceName, + &changed.ChangedProperties, + &changed.InvalidatedProperties); err != nil { + log.Printf("Failed to parse PropertiesChanged signal: %v", err) + break + } + if err := l.handlePropertiesChanged(signal.Path, &changed); err != nil { + log.Printf("Error in handling PropertiesChanged: %v", err) + } + case "org.freedesktop.DBus.ObjectManager.InterfacesAdded": + var added interfacesAddedSignal + if err := dbus.Store( + signal.Body, + &added.ObjectPath, + &added.InterfacesAndProperties); err != nil { + log.Printf("Failed to parse InterfacesAdded signal: %v", err) + break + } + if err := l.handleInterfacesAdded(&added); err != nil { + log.Printf("Error in handling InterfacesAdded: %v", err) + } + case "org.freedesktop.DBus.ObjectManager.InterfacesRemoved": + var removed interfacesRemovedSignal + if err := dbus.Store( + signal.Body, + &removed.ObjectPath, + &removed.Interfaces); err != nil { + log.Printf("Failed to parse InterfacesRemoved signal: %v", err) + break + } + l.m.Lock() + delete(l.advCache, removed.ObjectPath) + l.m.Unlock() + } + } +} + +// Issues a call to org.bluez.Adapter1.StartDiscovery retrying the +// "Resource Not Read" error that can happen transitively when the Bluetooth +// device is still powering on. +func (l *AdvListener) callAdapterStartDiscoveryWithRetry() error { + for retryNum := 0; true; retryNum++ { + time.Sleep(backoff.Exponential(retryNum, time.Second, time.Second*5, 2.0)) + call := l.adapter.Call("org.bluez.Adapter1.StartDiscovery", 0) + if call.Err == nil { + return nil + } + if call.Err.Error() != "Resource Not Ready" || retryNum > 5 { + return call.Err + } + log.Printf("Failed to StartDiscovery, retrying...") + } + panic("unreachable") +} + +func (l *AdvListener) startDiscoveryLoop() { + for { + if err := l.callAdapterStartDiscoveryWithRetry(); err != nil { + l.setError(fmt.Errorf("failed to start discovery: %v", err)) + return + } + // This block is responsible for making sure that adapter is constantly + // in the discovering mode. The single StartDiscovery should enable discovering + // indefinitely in the bluetooth daemon but it's possible that bluetooth deamon + // itself will be restarted/will crash and stop discovery. + // Instead of having this loop we could also listen to proper signals and detect + // changes that way, but this way it seems easier and good enough. + for discovering := true; discovering; { + time.Sleep(time.Minute * 4) + err := l.adapter.StoreProperty("org.bluez.Adapter1.Discovering", &discovering) + if err != nil { + l.setError(fmt.Errorf("failed to read discovering status: %v", err)) + return + } + } + // We should clear the map becuase cache might be stale. + l.m.Lock() + l.advCache = make(map[dbus.ObjectPath]Advertisement) + l.m.Unlock() + + log.Printf("Discovering stopped, restating...") + } +} + +// NewAdvListener creates new AdvListener and starts listening. +func NewAdvListener(adapterName string) (*AdvListener, error) { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return nil, fmt.Errorf("failed to conntect to system bus: %v", err) + } + adapterPath := dbus.ObjectPath(fmt.Sprintf("/org/bluez/%s", adapterName)) + + // This is just convenience to make the user interface of library better. + // Let's return that adapter doesn't exist as clear error before calling StartDiscovery. + var managedObjects map[dbus.ObjectPath]map[string]map[string]dbus.Variant + if err := conn.Object("org.bluez", dbus.ObjectPath("/")).Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&managedObjects); err != nil { + return nil, fmt.Errorf("failed to get list of Bluetooth adapters: %v", err) + } + if _, ok := managedObjects[adapterPath]; !ok { + return nil, fmt.Errorf("requested to listen on Bluetooth adapter '%s', but it doesn't exist", adapterName) + } + + l := &AdvListener{ + adapter: conn.Object("org.bluez", adapterPath), + conn: conn, + advCache: make(map[dbus.ObjectPath]Advertisement), + signals: make(chan *dbus.Signal, 10), + results: make(chan Advertisement, 10), + } + conn.Signal(l.signals) + go l.signalHandlerLoop() + go l.startDiscoveryLoop() + return l, nil +} diff --git a/pkg/config/BUILD.bazel b/pkg/config/BUILD.bazel new file mode 100644 index 0000000..0e65ca8 --- /dev/null +++ b/pkg/config/BUILD.bazel @@ -0,0 +1,27 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "config.go", + "config_format.go", + ], + importpath = "github.com/p2004a/gbcsdpd/pkg/config", + visibility = ["//visibility:public"], + deps = [ + "@com_github_dgrijalva_jwt_go//:go_default_library", + "@com_github_pelletier_go_toml//:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["config_test.go"], + data = glob(["testdata/**"]), + embed = [":go_default_library"], + deps = [ + "@com_github_dgrijalva_jwt_go//:go_default_library", + "@com_github_google_go_cmp//cmp:go_default_library", + "@com_github_google_go_cmp//cmp/cmpopts:go_default_library", + ], +) diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..ac7f07a --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,318 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "path" + "regexp" + "strings" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/pelletier/go-toml" +) + +// Sink represents the configuration for Sink. +type Sink interface{} + +// Config contains the full parsed configuration for the application. +type Config struct { + Adapter string + Sinks []Sink +} + +// RateLimit is configruation for the rate limiting of sinks. +type RateLimit struct { + Max1In time.Duration +} + +// PublicationFormat represents format of message published on MQTT topic. +type PublicationFormat int + +const ( + // BINARY means data is a proto serialized to binary. + BINARY PublicationFormat = iota + + // JSON means data is in json format. + JSON +) + +// MQTTSink is configuration for the sink.MQTTSink. +type MQTTSink struct { + Name, Topic, ClientID, UserName, Password string + Format PublicationFormat + RateLimit *RateLimit + ServerName string + ServerPort int + TLSConfig *tls.Config +} + +// GCPSink is configuration for the sink.GCPSink. +type GCPSink struct { + Name, Project, Region, Registry, Device string + Key *rsa.PrivateKey + RateLimit *RateLimit + ServerName string + ServerPort int + TLSConfig *tls.Config +} + +// StdoutSink is configuration for sink.StdoutSink. +type StdoutSink struct { + Name string + RateLimit *RateLimit +} + +var ( + projectIDRE, registryDeviceIDsRE, cloudIoTRegionRE, clientIDRE *regexp.Regexp +) + +func init() { + projectIDRE = regexp.MustCompile(`[-a-z0-9]{6,30}`) + registryDeviceIDsRE = regexp.MustCompile(`[a-zA-Z][-a-zA-Z0-9._+~%]{2,254}`) + cloudIoTRegionRE = regexp.MustCompile(`us-central1|europe-west1|asia-east1`) + clientIDRE = regexp.MustCompile(`[0-9a-zA-Z]{0,23}`) +} + +func joinPathWithAbs(basePath, filePath string) string { + if path.IsAbs(filePath) { + return filePath + } + return path.Join(basePath, filePath) +} + +func parseRateLimit(rateLimit *fRateLimit) (*RateLimit, error) { + if rateLimit == nil { + return nil, nil + } + res := &RateLimit{} + var err error + res.Max1In, err = time.ParseDuration(rateLimit.Max1In) + if err != nil { + return nil, fmt.Errorf("failed to parse max_1_in as duration: %v", err) + } + if res.Max1In < time.Second { + return nil, fmt.Errorf("max_1_in must be more then 1s") + } + return res, nil +} + +func parseTLSConfig(config *fTLSConfig, defaultServerName string, basePath string) (*tls.Config, error) { + res := &tls.Config{ + MinVersion: tls.VersionTLS12, + ClientSessionCache: tls.NewLRUClientSessionCache(10), + InsecureSkipVerify: config.SkipVerify, + } + if config.CACerts != nil { + certpool := x509.NewCertPool() + pemCerts, err := ioutil.ReadFile(joinPathWithAbs(basePath, *config.CACerts)) + if err != nil { + return nil, fmt.Errorf("failed to read '%s' cacerts file: %v", *config.CACerts, err) + } + if !certpool.AppendCertsFromPEM(pemCerts) { + return nil, fmt.Errorf("there weren't any falid certs to add in the given '%s' file", *config.CACerts) + } + res.RootCAs = certpool + } + if config.ServerName != nil { + res.ServerName = *config.ServerName + } else { + res.ServerName = defaultServerName + } + return res, nil +} + +func parseMQTTSink(basePath string, sinkID int, sink *fMQTTSink) (*MQTTSink, error) { + res := &MQTTSink{} + if sink.Name == "" { + res.Name = fmt.Sprintf("unnamed-mqtt-sink-%d", sinkID) + } else { + res.Name = sink.Name + } + + if len(sink.Topic) < 1 || len(sink.Topic) > 65535 || sink.Topic[0] == '$' || strings.ContainsAny(sink.Topic, "+#\u0000") { + return nil, fmt.Errorf("sink %s: Topic is not in valid format, see https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718106, given: '%s'", sink.Name, sink.Topic) + } + res.Topic = sink.Topic + + if !clientIDRE.MatchString(sink.ClientID) { + return nil, fmt.Errorf("sink %s: Client ID is not in valid format, see https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718031, given: '%s'", res.Name, sink.ClientID) + } + res.ClientID = sink.ClientID + + if len(sink.UserName) > 65535 || len(sink.Password) > 65535 { + return nil, fmt.Errorf("sink %s: Max len of UserName and Password fields is 65535", res.Name) + } + res.UserName = sink.UserName + res.Password = sink.Password + + if sink.Format == "BINARY" { + res.Format = BINARY + } else if sink.Format == "JSON" { + res.Format = JSON + } else { + return nil, fmt.Errorf("sink %s: Format have to be either BINARY or JSON, given: '%s'", res.Name, sink.Format) + } + + rateLimit, err := parseRateLimit(sink.RateLimit) + if err != nil { + return nil, fmt.Errorf("sink %s: Failed to parse rate limit: %v", sink.Name, err) + } + res.RateLimit = rateLimit + + if len(sink.ServerName) == 0 { + return nil, fmt.Errorf("sink %s: server_name is a required field", sink.Name) + } + res.ServerName = sink.ServerName + res.ServerPort = sink.ServerPort + + if sink.EnableTLS { + tlsConfig, err := parseTLSConfig(&sink.TLS, res.ServerName, basePath) + if err != nil { + return nil, fmt.Errorf("sink %s: Failed to parse tls config: %v", sink.Name, err) + } + res.TLSConfig = tlsConfig + } + + return res, nil +} + +func parseGCPSink(basePath string, sinkID int, sink *fGCPSink) (*GCPSink, error) { + res := &GCPSink{} + if sink.Name == "" { + res.Name = fmt.Sprintf("unnamed-gcp-sink-%d", sinkID) + } else { + res.Name = sink.Name + } + + if !projectIDRE.MatchString(sink.Project) { + return nil, fmt.Errorf("sink %s: Project ID must meet requirements in https://cloud.google.com/resource-manager/docs/creating-managing-projects#before_you_begin, given: '%s'", res.Name, sink.Project) + } + res.Project = sink.Project + + if !cloudIoTRegionRE.MatchString(sink.Region) { + return nil, fmt.Errorf("sink %s: Region must be one of us-central1, europe-west1, and asia-east1. See https://cloud.google.com/iot/docs/requirements#permitted_characters_and_size_requirements, given: '%s'", res.Name, sink.Region) + } + res.Region = sink.Region + + if !registryDeviceIDsRE.MatchString(sink.Registry) { + return nil, fmt.Errorf("sink %s: Registry ID much meet requirements in https://cloud.google.com/iot/docs/requirements#permitted_characters_and_size_requirements, given: '%s'", res.Name, sink.Registry) + } + res.Registry = sink.Registry + + if !registryDeviceIDsRE.MatchString(sink.Device) { + return nil, fmt.Errorf("sink %s: Device ID much meet requirements in https://cloud.google.com/iot/docs/requirements#permitted_characters_and_size_requirements, given: '%s'", res.Name, sink.Device) + } + res.Device = sink.Device + + if sink.Key == "" { + return nil, fmt.Errorf("sink %s: The private key path must be specified", res.Name) + } else if privateKeyBytes, err := ioutil.ReadFile(joinPathWithAbs(basePath, sink.Key)); err != nil { + return nil, fmt.Errorf("sink %s: Failed to read '%s' key file: %v", sink.Name, sink.Key, err) + } else if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyBytes); err != nil { + return nil, fmt.Errorf("sink %s: Failed to parse '%s' PEM key file: %v", sink.Name, sink.Key, err) + } else { + res.Key = privateKey + } + + rateLimit, err := parseRateLimit(sink.RateLimit) + if err != nil { + return nil, fmt.Errorf("sink %s: Failed to parse rate limit: %v", sink.Name, err) + } + res.RateLimit = rateLimit + + res.ServerName = sink.ServerName + res.ServerPort = sink.ServerPort + + tlsConfig, err := parseTLSConfig(&sink.TLS, res.ServerName, basePath) + if err != nil { + return nil, fmt.Errorf("sink %s: Failed to parse tls config: %v", sink.Name, err) + } + res.TLSConfig = tlsConfig + + return res, nil +} + +func parseStdoutSink(sinkID int, sink *fStdoutSink) (*StdoutSink, error) { + res := &StdoutSink{} + if sink.Name == "" { + res.Name = fmt.Sprintf("unnamed-stdout-sink-%d", sinkID) + } else { + res.Name = sink.Name + } + rateLimit, err := parseRateLimit(sink.RateLimit) + if err != nil { + return nil, fmt.Errorf("sink %s: Failed to parse rate limit: %v", sink.Name, err) + } + res.RateLimit = rateLimit + return res, nil +} + +// Read reads a configuration file defined in config_format.go and +// parses it into easily digestable Config struct. +func Read(configPath string) (*Config, error) { + fconfig := fConfig{} + // If config path is empty, assume empty config file. + if configPath != "" { + configBytes, err := ioutil.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read '%s' file: %v", configPath, err) + } + if err := toml.Unmarshal(configBytes, &fconfig); err != nil { + return nil, fmt.Errorf("failed to parse '%s' file: %v", configPath, err) + } + } else { + if toml.Unmarshal([]byte{}, &fconfig) != nil { + panic("failed to parse empty config") + } + } + + config := &Config{} + config.Adapter = fconfig.Adapter + for i, sink := range fconfig.Sinks.MQTT { + mqttSink, err := parseMQTTSink(path.Dir(configPath), i, sink) + if err != nil { + return nil, fmt.Errorf("failed to parse MQTT sink conifg: %v", err) + } + config.Sinks = append(config.Sinks, mqttSink) + } + for i, sink := range fconfig.Sinks.GCP { + gcpSink, err := parseGCPSink(path.Dir(configPath), i, sink) + if err != nil { + return nil, fmt.Errorf("failed to parse GCP sink conifg: %v", err) + } + config.Sinks = append(config.Sinks, gcpSink) + } + for i, sink := range fconfig.Sinks.Stdout { + stdoutSink, err := parseStdoutSink(i, sink) + if err != nil { + return nil, fmt.Errorf("failed to parse stdout sink conifg: %v", err) + } + config.Sinks = append(config.Sinks, stdoutSink) + } + + if len(config.Sinks) == 0 { + config.Sinks = append(config.Sinks, &StdoutSink{ + Name: "default-sink", + }) + } + + return config, nil +} diff --git a/pkg/config/config_format.go b/pkg/config/config_format.go new file mode 100644 index 0000000..92692da --- /dev/null +++ b/pkg/config/config_format.go @@ -0,0 +1,125 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +// Represents the config file, root for unmarshaling the TOML config +type fConfig struct { + // Name of the bluetooth adapter to listen for publications, eg hci0 + Adapter string `toml:"adapter" default:"hci0"` + + // If none sinks are defined, a single default Stdout sink is created + Sinks fSinks `toml:"sinks"` +} + +// Struct holds list of sinks for publications +type fSinks struct { + MQTT []*fMQTTSink `toml:"mqtt"` + GCP []*fGCPSink `toml:"gcp"` + Stdout []*fStdoutSink `toml:"stdout"` +} + +// Configruation for publishing to stdout +type fStdoutSink struct { + // Optional name of sink + Name string `toml:"name"` + + RateLimit *fRateLimit `toml:"rate_limit"` +} + +// Configuration for publishing to generic MQTT server +type fMQTTSink struct { + // Optional name of sink + Name string `toml:"name"` + + RateLimit *fRateLimit `toml:"rate_limit"` + + // MQTT topic name + Topic string `toml:"topic"` + + // Client ID to send to the server, can be left as an empty string + ClientID string `toml:"client_id"` + + // Username for authentication + UserName string `toml:"username"` + + // Password for authentication. Spec allows for binary data here, but this config + // doesn't as TOML doesn't have a native type for it + Password string `toml:"password"` + + // Format of published `MeasurementsPublication` message. Can be either BINARY or JSON + Format string `toml:"format" default:"BINARY"` + + // Server name to connect to + ServerName string `toml:"server_name"` + + // Server port to connect to + ServerPort int `toml:"server_port" default:"8883"` + + // Whatever to use TLS to connect to the server + EnableTLS bool `toml:"enable_tls" default:"true"` + + // TLS configuration for connection, used when EnableTLS is true. + TLS fTLSConfig `toml:"tls"` +} + +// Configruation for publishing to Google Cloud IoT Core +type fGCPSink struct { + // Optional name of sink + Name string `toml:"name"` + + RateLimit *fRateLimit `toml:"rate_limit"` + + // Project Id + Project string `toml:"project"` + + // Region to contact + Region string `toml:"region"` + + // Device registry ID + Registry string `toml:"registry"` + + // Device ID + Device string `toml:"device"` + + // Path to device private key in PEM format + Key string `toml:"key"` + + // GCP server name to connect to + ServerName string `toml:"server_name" default:"mqtt.googleapis.com"` + + // GCP server port to connect to + ServerPort int `toml:"server_port" default:"8883"` + + // TLS configuration for connecting to GCP + TLS fTLSConfig `toml:"tls"` +} + +// Configuration for publishing rate limitting +type fRateLimit struct { + // Specifies rate limit to publish max 1 publication in duration. + // Duration is string in the format for `time.ParseDuration`, eg: 60s, 1m10s + Max1In string `toml:"max_1_in"` +} + +type fTLSConfig struct { + // root certificate authorities, if empty, the systems default is used + CACerts *string `toml:"ca_certs"` + + // Controls if we should verify the server certificate + SkipVerify bool `toml:"skip_verify" default:"false"` + + // Allows to set the server name, by default inherits ServerName from parent + ServerName *string `toml:"server_name"` +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..c683a37 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,168 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "io/ioutil" + "testing" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func readKey(t *testing.T, keyPath string) *rsa.PrivateKey { + privateKeyBytes, err := ioutil.ReadFile(keyPath) + if err != nil { + t.Fatalf("Failed to load file %s: %v", keyPath, err) + } + privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyBytes) + if err != nil { + t.Fatalf("Failed to parse pem file %s: %v", keyPath, err) + } + return privateKey +} + +func readCACerts(t *testing.T, caCertsPath string) *x509.CertPool { + certpool := x509.NewCertPool() + pemCerts, err := ioutil.ReadFile(caCertsPath) + if err != nil { + t.Fatalf("Failed to load file %s: %v", caCertsPath, err) + } + if !certpool.AppendCertsFromPEM(pemCerts) { + t.Fatalf("There were no certs in %s", caCertsPath) + } + return certpool +} + +func caCertsTrans(cp *x509.CertPool) [][]byte { + if cp == nil { + return [][]byte{} + } + return cp.Subjects() +} + +func cmpConfig(actual, expected *Config) string { + return cmp.Diff(actual, expected, + cmp.Transformer("CertPool", caCertsTrans), + cmpopts.IgnoreUnexported(tls.Config{}), + cmpopts.IgnoreFields(tls.Config{}, "ClientSessionCache")) +} + +func TestParsingCorrect(t *testing.T) { + config, err := Read("testdata/test1/config.toml") + if err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + expectedConfig := &Config{ + Adapter: "hci1", + Sinks: []Sink{ + &MQTTSink{ + Name: "mqtt sink 1", + RateLimit: &RateLimit{Max1In: 5 * time.Second}, + Topic: "/measurements", + ClientID: "my-pusher", + UserName: "alibaba", + Password: "open sesame", + Format: JSON, + ServerName: "localhost", + ServerPort: 8883, + TLSConfig: nil, + }, + &GCPSink{ + Name: "gcp sink 1", + Project: "project1", + Region: "europe-west1", + Registry: "registry1", + Device: "device1", + ServerName: "test.gcp.com", + ServerPort: 9999, + Key: readKey(t, "testdata/test1/key.pem"), + RateLimit: &RateLimit{Max1In: 60 * time.Second}, + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + ClientSessionCache: tls.NewLRUClientSessionCache(10), + InsecureSkipVerify: true, + ServerName: "tls_overriden.gcp.com", + RootCAs: readCACerts(t, "testdata/test1/myCa.pem"), + }, + }, + &StdoutSink{ + Name: "stdout sink 1", + RateLimit: &RateLimit{Max1In: 90 * time.Second}, + }, + &StdoutSink{ + Name: "stdout sink 2", + RateLimit: &RateLimit{Max1In: 10 * time.Second}, + }, + }, + } + if diff := cmpConfig(config, expectedConfig); diff != "" { + t.Errorf("unexpected difference:\n%v", diff) + } +} + +func TestBaseGCP(t *testing.T) { + config, err := Read("testdata/gcpbase/config.toml") + if err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + expectedConfig := &Config{ + Adapter: "hci0", + Sinks: []Sink{ + &GCPSink{ + Name: "unnamed-gcp-sink-0", + Project: "project2", + Region: "asia-east1", + Registry: "registry2", + Device: "device2", + ServerName: "mqtt.googleapis.com", + ServerPort: 8883, + Key: readKey(t, "testdata/gcpbase/key.pem"), + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + ClientSessionCache: tls.NewLRUClientSessionCache(10), + InsecureSkipVerify: false, + ServerName: "mqtt.googleapis.com", + }, + }, + }, + } + if diff := cmpConfig(config, expectedConfig); diff != "" { + t.Errorf("unexpected difference:\n%v", diff) + } +} + +func TestParsingEmpty(t *testing.T) { + config, err := Read("") + if err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + expectedConfig := &Config{ + Adapter: "hci0", + Sinks: []Sink{ + &StdoutSink{ + Name: "default-sink", + }, + }, + } + if diff := cmpConfig(config, expectedConfig); diff != "" { + t.Errorf("unexpected difference:\n%v", diff) + } +} diff --git a/pkg/config/testdata/gcpbase/config.toml b/pkg/config/testdata/gcpbase/config.toml new file mode 100644 index 0000000..6486f5f --- /dev/null +++ b/pkg/config/testdata/gcpbase/config.toml @@ -0,0 +1,6 @@ +[[sinks.gcp]] +project = "project2" +region = "asia-east1" +registry = "registry2" +device = "device2" +key = "key.pem" diff --git a/pkg/config/testdata/gcpbase/key.pem b/pkg/config/testdata/gcpbase/key.pem new file mode 100644 index 0000000..a5d2cbc --- /dev/null +++ b/pkg/config/testdata/gcpbase/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCqhxsNyCVlVlkb +lgH8gzRPWlm0dsD4Exbeec5nJcCSZkWVXHoC28VfuYfgLrXh6l+bS9ArQjc8GrUh +kbUmZDzRmgjKcSY9nTCGBs1/It0NNIeBwRAkvjTD+lqngy3qP34sS1d/leWOquSH +nfyzrVBNniqG3YVo1slrt7sACE2XmFovez1P9QQshfS3AK/DjGZO6AMTUAoajSJK +eFaUzv6SJwjx4EeRIgaIQlHdE/ix1ndkk8OQ/4sZwUy+smFC3UC7HbBDPsZyvfoc +lkC5iJH2KCA4/ty7H4TTs8mOQwd9Psn/GSQvvThhWs+gWZvZmRIJLcjOU5txRXbv +jm+Zpks9AgMBAAECggEAYGrZmhZDRqPm6BkN8HdC2Wctd0L54onwkUPvtxR6aIxY +5ZWPCxS16WTedZwTjLPW8NiR0BO1ZU94gI2BDj74wE5GkCgfxhCdgfpQsITG1ZOQ +1oWRmiTNcs2X+kTKbjsOHP9QbrwTOnJXmnJykij5UZmPVAfmSZu/8R7GJcOME5zM +tZWWjWKAby6mkw6q8wAVyITVlrelt4gzFHzkRN8q2n9/wmHGicfLwHWcEJrcdYxP +fr4FAI1FtxMuxJCWVeh5rJVoFP7rq66kBjghoOLKuEWDm48pfIgpvaeSMtt9Zlmt +g8kMbZlJ148twXTGrHHE62mABEgwOjutooXVCLVGZQKBgQDi5NDwfv33RaHdtnLx +MDZlYi4tErUwUaxKYDCOvXif02m58wyWmRtmMdmLGjZL8uYu0WQXrDiQQtwEUZGM +bHWUMT1+n4TkdA7ur50HHk0RIl8DyNvM6gqxeg8xoOGXcugzaP619TTfU/qoG8/D ++rZyMCUG0Iasuk6TnoSMFAFW2wKBgQDAZzr+blDplktJOSB/d6HMxihWy+dxpHhk +7D/ivr1U44TJgd+Wqe2eyAT/geI4YXII16BsITnea/ifDjqEuK2pVuc3JJmbnoNi +TYuGDOsTUV6RFUoxKrk3CxGXnQkBUs821Ep/3m2vMLod2uSUns3xvd3TEG6Fs0hi +H5jcAjSFxwKBgQCAhcKA6D5tyei1kTqsunWlmiaz62vtEeZ5PuFiiZsBVZ0G8tEH +oXSuv8ANlmx5Ov7+OCftbOWhee3tGFNM6sbziazew/df/QnUVG+rb5OSCBkwKJ+x +BEXIYG6o2wvOYQ18yZW2dk5bztMmVJKs3aBpMDJZGNegkewenGVSf6Z+jwKBgBln +R93CGQLOakBPv5+03vMXksnrADL8AT1qCAFbJ8pmg+jLMgdFhm85f5dwwbqp+xF5 +zt+X/3kDjn8JtOZDMAK0y7B3L6ThZ/15uZtIZ11UmATV58bYGj5PQtJe1IqNMXjO +zMtXReokp947QYTx9sUdSYWNnNogUsVJ4LfjvqWPAoGBALCvHBdmXJ4Po+svY3Vc +LNu2HNwHnlxYRU9M0HTNdlo/yeJnwK6dZ41WIh1swhF2XnQ56U84i2XlUzkwtEQm +RvJEhE57yIZsSAAaVGxj7psqxEj7nFx9k81/nltQKcHAg2CBO1gSvAtTnaUHvVpT +bqOvAV++SsdBdWs6fdw5EH7/ +-----END PRIVATE KEY----- diff --git a/pkg/config/testdata/test1/config.toml b/pkg/config/testdata/test1/config.toml new file mode 100644 index 0000000..65182c7 --- /dev/null +++ b/pkg/config/testdata/test1/config.toml @@ -0,0 +1,34 @@ +adapter = "hci1" + +[[sinks.stdout]] +name = "stdout sink 1" +rate_limit.max_1_in = "90s" + +[[sinks.mqtt]] +name = "mqtt sink 1" +rate_limit.max_1_in = "5s" +topic = "/measurements" +client_id = "my-pusher" +username = "alibaba" +password = "open sesame" +format = "JSON" +server_name = "localhost" +enable_tls = false + +[[sinks.gcp]] +name = "gcp sink 1" +rate_limit.max_1_in = "60s" +project = "project1" +region = "europe-west1" +registry = "registry1" +device = "device1" +key = "key.pem" +server_name = "test.gcp.com" +server_port = 9999 +tls.ca_certs = "myCa.pem" +tls.skip_verify = true +tls.server_name = "tls_overriden.gcp.com" + +[[sinks.stdout]] +name = "stdout sink 2" +rate_limit.max_1_in = "10s" diff --git a/pkg/config/testdata/test1/key.pem b/pkg/config/testdata/test1/key.pem new file mode 100644 index 0000000..a5d2cbc --- /dev/null +++ b/pkg/config/testdata/test1/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCqhxsNyCVlVlkb +lgH8gzRPWlm0dsD4Exbeec5nJcCSZkWVXHoC28VfuYfgLrXh6l+bS9ArQjc8GrUh +kbUmZDzRmgjKcSY9nTCGBs1/It0NNIeBwRAkvjTD+lqngy3qP34sS1d/leWOquSH +nfyzrVBNniqG3YVo1slrt7sACE2XmFovez1P9QQshfS3AK/DjGZO6AMTUAoajSJK +eFaUzv6SJwjx4EeRIgaIQlHdE/ix1ndkk8OQ/4sZwUy+smFC3UC7HbBDPsZyvfoc +lkC5iJH2KCA4/ty7H4TTs8mOQwd9Psn/GSQvvThhWs+gWZvZmRIJLcjOU5txRXbv +jm+Zpks9AgMBAAECggEAYGrZmhZDRqPm6BkN8HdC2Wctd0L54onwkUPvtxR6aIxY +5ZWPCxS16WTedZwTjLPW8NiR0BO1ZU94gI2BDj74wE5GkCgfxhCdgfpQsITG1ZOQ +1oWRmiTNcs2X+kTKbjsOHP9QbrwTOnJXmnJykij5UZmPVAfmSZu/8R7GJcOME5zM +tZWWjWKAby6mkw6q8wAVyITVlrelt4gzFHzkRN8q2n9/wmHGicfLwHWcEJrcdYxP +fr4FAI1FtxMuxJCWVeh5rJVoFP7rq66kBjghoOLKuEWDm48pfIgpvaeSMtt9Zlmt +g8kMbZlJ148twXTGrHHE62mABEgwOjutooXVCLVGZQKBgQDi5NDwfv33RaHdtnLx +MDZlYi4tErUwUaxKYDCOvXif02m58wyWmRtmMdmLGjZL8uYu0WQXrDiQQtwEUZGM +bHWUMT1+n4TkdA7ur50HHk0RIl8DyNvM6gqxeg8xoOGXcugzaP619TTfU/qoG8/D ++rZyMCUG0Iasuk6TnoSMFAFW2wKBgQDAZzr+blDplktJOSB/d6HMxihWy+dxpHhk +7D/ivr1U44TJgd+Wqe2eyAT/geI4YXII16BsITnea/ifDjqEuK2pVuc3JJmbnoNi +TYuGDOsTUV6RFUoxKrk3CxGXnQkBUs821Ep/3m2vMLod2uSUns3xvd3TEG6Fs0hi +H5jcAjSFxwKBgQCAhcKA6D5tyei1kTqsunWlmiaz62vtEeZ5PuFiiZsBVZ0G8tEH +oXSuv8ANlmx5Ov7+OCftbOWhee3tGFNM6sbziazew/df/QnUVG+rb5OSCBkwKJ+x +BEXIYG6o2wvOYQ18yZW2dk5bztMmVJKs3aBpMDJZGNegkewenGVSf6Z+jwKBgBln +R93CGQLOakBPv5+03vMXksnrADL8AT1qCAFbJ8pmg+jLMgdFhm85f5dwwbqp+xF5 +zt+X/3kDjn8JtOZDMAK0y7B3L6ThZ/15uZtIZ11UmATV58bYGj5PQtJe1IqNMXjO +zMtXReokp947QYTx9sUdSYWNnNogUsVJ4LfjvqWPAoGBALCvHBdmXJ4Po+svY3Vc +LNu2HNwHnlxYRU9M0HTNdlo/yeJnwK6dZ41WIh1swhF2XnQ56U84i2XlUzkwtEQm +RvJEhE57yIZsSAAaVGxj7psqxEj7nFx9k81/nltQKcHAg2CBO1gSvAtTnaUHvVpT +bqOvAV++SsdBdWs6fdw5EH7/ +-----END PRIVATE KEY----- diff --git a/pkg/config/testdata/test1/myCa.pem b/pkg/config/testdata/test1/myCa.pem new file mode 100644 index 0000000..c9604b6 --- /dev/null +++ b/pkg/config/testdata/test1/myCa.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUPFC7MZtv1bl4l7+T+ePna5L5U/YwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDA5MTQxODA1MzdaFw0yNTA5 +MTMxODA1MzdaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDBCfPRN6YM2F3oUwzG0CoVZ3nPlAQUS9B+r0aZ3Orz +YiJm9Zt/L6bZVoTIrFRwweFe8hkUOtlBPkVtFW1uM+uI3S4ln3hpmMni9lvQHMGF +I06x/A7kbX7nUBNWAlMc2HDRXwi7LUeoparhi8kinmrRBX0PqJHcNFglBSU9Ol33 +ghx9X5NOnotkKNvr6GC2Qrxl1VGplAxT/CMMhetdHlt89y17ZzVSvdLxBWACfeFn +x2wSBNpu4a9r65JAQKClve1EgqH0vXbsJ5HxJSsl/uStY7VMEM2IAvaAPz1uQbOb +etobDZUzz4fMkLPQI22Yh/Pl5j9Y3in0KeSZV1jQIfqnAgMBAAGjUzBRMB0GA1Ud +DgQWBBSKwcmF2bYRw06qug7o5CU319su6TAfBgNVHSMEGDAWgBSKwcmF2bYRw06q +ug7o5CU319su6TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCw +yRMwfLsK0TPJW6FUcrkzcunv4W89RTceUJUHVOUZId+UEGa43iElRpp/KfEG2xaJ +2KYOigYz6YOGV5F4HAnh+MPgVTKB1nQCRaHLqh0qBl/bRLUpv/fAMeutgdviVH6q +pHapuWHNGZqssByY8DUp4pFpYeqjHp4o01oBdJhROps3yAm+spHTyQBYeivGAG0K +zS2uFLbhZzs7C44RWfCzkIhHDBm0vgkodTKh2TopzsV+vVVgtBHiPJqvS8rNf4LI +ADONrLqwaJgPP739rqoKSlDGRMfadywgMwwvut1AA6TMnAdgvrirO/uoYkKzEr1Y +Ktt4U+VGX1lrsSXm3h3o +-----END CERTIFICATE----- diff --git a/pkg/ruuviparse/BUILD.bazel b/pkg/ruuviparse/BUILD.bazel new file mode 100644 index 0000000..5f8a826 --- /dev/null +++ b/pkg/ruuviparse/BUILD.bazel @@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["ruuviparse.go"], + importpath = "github.com/p2004a/gbcsdpd/pkg/ruuviparse", + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["ruuviparse_test.go"], + embed = [":go_default_library"], +) diff --git a/pkg/ruuviparse/ruuviparse.go b/pkg/ruuviparse/ruuviparse.go new file mode 100644 index 0000000..5790a51 --- /dev/null +++ b/pkg/ruuviparse/ruuviparse.go @@ -0,0 +1,185 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ruuviparse + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" +) + +// RuuviDataFormat represents version of Ruuvi data format. +type RuuviDataFormat uint8 + +// List of allowed RuuviDataFormat values. +const ( + UNSPECIFIED RuuviDataFormat = iota + RAWv1 + RAWv2 +) + +// RuuviData contains a parsed data from Ruuvi sensor. +type RuuviData struct { + DataFormat RuuviDataFormat + Temperature *float32 // C + Humidity *float32 // RH % + Pressure *float32 // hPa + Acceleration [3]*float32 // G + BatteryVoltage *float32 // V + TxPower *float32 // dBm + MovementCounter *uint // counter + MeasurementSeqNum *uint // counter + Mac net.HardwareAddr // MAC +} + +// Parse takes Manufacturer Specific Data field from BLE advertisement for the Ruuvi +// manufacturer and parses the content into RuuviData. +func Parse(data []byte) (rd *RuuviData, err error) { + if len(data) < 1 { + return nil, fmt.Errorf("got empty byte slice") + } + switch data[0] { + case 3: + rd, err = parseRAWv1(data) + if err != nil { + err = fmt.Errorf("failed to parse data in RAWv1 format: %v", err) + } + return parseRAWv1(data) + case 5: + rd, err = parseRAWv2(data) + if err != nil { + err = fmt.Errorf("failed to parse data in RAWv2 format: %v", err) + } + default: + err = fmt.Errorf("only RAWv1 and RAWv2 Ruuvi formats supported, got: %d", data[0]) + } + return +} + +func nilF32(v float32) *float32 { return &v } +func nilUint(v uint) *uint { return &v } + +func parseRAWv1(data []byte) (*RuuviData, error) { + rd := struct { + DataFormat uint8 + Humidity uint8 + Temperature uint8 + TemperatureFraction uint8 + Pressure uint16 + AccelX int16 + AccelY int16 + AccelZ int16 + BatteryVoltage uint16 + }{} + dataExpectedSize := binary.Size(rd) + if len(data) != dataExpectedSize { + return nil, fmt.Errorf("Ruuvi manufacturer data must be exactly %d bytes", dataExpectedSize) + } + + buffer := bytes.NewBuffer(data) + err := binary.Read(buffer, binary.BigEndian, &rd) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal struct: %v", err) + } + + var temp *float32 = nil + if rd.TemperatureFraction < 100 { + temp = nilF32(float32(rd.Temperature&0x7F) + float32(rd.TemperatureFraction)/100.0) + if rd.Temperature&0x80 != 0 { + *temp *= -1 + } + } + + return &RuuviData{ + DataFormat: RAWv1, + Temperature: temp, + Humidity: nilF32(float32(rd.Humidity) / 2.0), + Pressure: nilF32((float32(rd.Pressure) + 50000.0) / 100.0), + Acceleration: [3]*float32{ + nilF32(float32(rd.AccelX) / 1000.0), + nilF32(float32(rd.AccelY) / 1000.0), + nilF32(float32(rd.AccelZ) / 1000.0), + }, + BatteryVoltage: nilF32(float32(rd.BatteryVoltage) / 1000.0), + }, nil +} + +func nilInvF32(invalid int, current int, v float32) *float32 { + if current == invalid { + return nil + } + return &v +} + +func nilInvUint(invalid int, current int, v uint) *uint { + if current == invalid { + return nil + } + return &v +} + +// https://github.com/ruuvi/ruuvi-sensor-protocols/blob/master/dataformat_05.md +func parseRAWv2(data []byte) (*RuuviData, error) { + rd := struct { + DataFormat uint8 + Temperature int16 + Humidity uint16 + Pressure uint16 + AccelX int16 + AccelY int16 + AccelZ int16 + PowerInfo uint16 + MovementCounter uint8 + MeasurementSeqNum uint16 + Mac [6]byte + }{} + dataExpectedSize := binary.Size(rd) + if len(data) != dataExpectedSize { + return nil, fmt.Errorf("Ruuvi manufacturer data must be exactly %d bytes", dataExpectedSize) + } + + buffer := bytes.NewBuffer(data) + err := binary.Read(buffer, binary.BigEndian, &rd) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal struct: %v", err) + } + + // This is mostly for test vector MAC validation, might want to add more checks. + mac := rd.Mac[:] + if rd.Mac == [6]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} { + mac = nil + } + + battery := rd.PowerInfo >> 5 + txPower := rd.PowerInfo & 0x001F + + return &RuuviData{ + DataFormat: RAWv2, + Temperature: nilInvF32(-32768, int(rd.Temperature), float32(rd.Temperature)*0.005), + Humidity: nilInvF32(65535, int(rd.Humidity), float32(rd.Humidity)*0.0025), + Pressure: nilInvF32(65535, int(rd.Pressure), (float32(rd.Pressure)+50000.0)/100.0), + Acceleration: [3]*float32{ + nilInvF32(-32768, int(rd.AccelX), float32(rd.AccelX)/1000.0), + nilInvF32(-32768, int(rd.AccelY), float32(rd.AccelY)/1000.0), + nilInvF32(-32768, int(rd.AccelZ), float32(rd.AccelZ)/1000.0), + }, + BatteryVoltage: nilInvF32(2047, int(battery), (float32(battery)+1600.0)/1000.0), + TxPower: nilInvF32(31, int(txPower), float32(txPower)*2.0-40.0), + MovementCounter: nilInvUint(255, int(rd.MovementCounter), uint(rd.MovementCounter)), + MeasurementSeqNum: nilInvUint(65535, int(rd.MeasurementSeqNum), uint(rd.MeasurementSeqNum)), + Mac: mac, + }, nil +} diff --git a/pkg/ruuviparse/ruuviparse_test.go b/pkg/ruuviparse/ruuviparse_test.go new file mode 100644 index 0000000..cae9137 --- /dev/null +++ b/pkg/ruuviparse/ruuviparse_test.go @@ -0,0 +1,189 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ruuviparse + +import ( + "encoding/hex" + "fmt" + "math" + "net" + "testing" +) + +func toB(s string) []byte { + decoded, err := hex.DecodeString(s) + if err != nil { + panic("Got invalid hex string") + } + return decoded +} + +func f32Ptr(v float32) *float32 { return &v } +func uintPtr(v uint) *uint { return &v } + +func f32PtrToStr(v *float32) string { + if v == nil { + return "nil" + } + return fmt.Sprint(*v) +} + +func uintPtrToStr(v *uint) string { + if v == nil { + return "nil" + } + return fmt.Sprint(*v) +} + +func macToStr(mac net.HardwareAddr) string { + if mac == nil { + return "nil" + } + return mac.String() +} + +func assertF32Eq(t *testing.T, param string, value, expected *float32) { + if value == nil || expected == nil { + if value != expected { + t.Errorf("%s not equal: value %s, expected %s", param, f32PtrToStr(value), f32PtrToStr(expected)) + } + } else if math.Abs(float64(*value-*expected)) > 0.0001 { + t.Errorf("%s not equal: value %s, expected %s", param, f32PtrToStr(value), f32PtrToStr(expected)) + } +} + +func assertUintEq(t *testing.T, param string, value, expected *uint) { + if value == nil || expected == nil { + if value != expected { + t.Errorf("%s not equal: value %s, expected %s", param, uintPtrToStr(value), uintPtrToStr(expected)) + } + } else if *value != *expected { + t.Errorf("%s not equal: value %s, expected %s", param, uintPtrToStr(value), uintPtrToStr(expected)) + } +} + +func TestParsingValid(t *testing.T) { + cases := []struct { + name string + data []byte + expected *RuuviData + }{ + {"RAWv2 valid", toB("0512FC5394C37C0004FFFC040CAC364200CDCBB8334C884F"), &RuuviData{ + DataFormat: RAWv2, + Temperature: f32Ptr(24.3), + Pressure: f32Ptr(1000.44), + Humidity: f32Ptr(53.49), + Acceleration: [3]*float32{f32Ptr(0.004), f32Ptr(-0.004), f32Ptr(1.036)}, + TxPower: f32Ptr(4.0), + BatteryVoltage: f32Ptr(2.977), + MovementCounter: uintPtr(66), + MeasurementSeqNum: uintPtr(205), + Mac: []byte{0xCB, 0xB8, 0x33, 0x4C, 0x88, 0x4F}, + }}, + {"RAWv2 maximum", toB("057FFFFFFEFFFE7FFF7FFF7FFFFFDEFEFFFECBB8334C884F"), &RuuviData{ + DataFormat: RAWv2, + Temperature: f32Ptr(163.835), + Pressure: f32Ptr(1155.34), + Humidity: f32Ptr(163.8350), + Acceleration: [3]*float32{f32Ptr(32.767), f32Ptr(32.767), f32Ptr(32.767)}, + TxPower: f32Ptr(20), + BatteryVoltage: f32Ptr(3.646), + MovementCounter: uintPtr(254), + MeasurementSeqNum: uintPtr(65534), + Mac: []byte{0xCB, 0xB8, 0x33, 0x4C, 0x88, 0x4F}, + }}, + {"RAWv2 minimum", toB("058001000000008001800180010000000000CBB8334C884F"), &RuuviData{ + DataFormat: RAWv2, + Temperature: f32Ptr(-163.835), + Pressure: f32Ptr(500.00), + Humidity: f32Ptr(0.0), + Acceleration: [3]*float32{f32Ptr(-32.767), f32Ptr(-32.767), f32Ptr(-32.767)}, + TxPower: f32Ptr(-40.0), + BatteryVoltage: f32Ptr(1.600), + MovementCounter: uintPtr(0), + MeasurementSeqNum: uintPtr(0), + Mac: []byte{0xCB, 0xB8, 0x33, 0x4C, 0x88, 0x4F}, + }}, + {"RAWv2 invalid", toB("058000FFFFFFFF800080008000FFFFFFFFFFFFFFFFFFFFFF"), &RuuviData{ + DataFormat: RAWv2, + Acceleration: [3]*float32{}, + }}, + {"RAWv1 valid", toB("03291A1ECE1EFC18F94202CA0B53"), &RuuviData{ + DataFormat: RAWv1, + Temperature: f32Ptr(26.3), + Pressure: f32Ptr(1027.66), + Humidity: f32Ptr(20.5), + Acceleration: [3]*float32{f32Ptr(-1.000), f32Ptr(-1.726), f32Ptr(0.714)}, + BatteryVoltage: f32Ptr(2.899), + }}, + {"RAWv1 maximum", toB("03FF7F63FFFF7FFF7FFF7FFFFFFF"), &RuuviData{ + DataFormat: RAWv1, + Temperature: f32Ptr(127.99), + Pressure: f32Ptr(1155.35), + Humidity: f32Ptr(127.5), + Acceleration: [3]*float32{f32Ptr(32.767), f32Ptr(32.767), f32Ptr(32.767)}, + BatteryVoltage: f32Ptr(65.535), + }}, + {"RAWv1 minimum", toB("0300FF6300008001800180010000"), &RuuviData{ + DataFormat: RAWv1, + Temperature: f32Ptr(-127.99), + Pressure: f32Ptr(500.00), + Humidity: f32Ptr(0.0), + Acceleration: [3]*float32{f32Ptr(-32.767), f32Ptr(-32.767), f32Ptr(-32.767)}, + BatteryVoltage: f32Ptr(0.0), + }}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + res, err := Parse(tc.data) + if err != nil { + t.Fatalf("couldn't parse data: %v", err) + } + assertF32Eq(t, "temperature", res.Temperature, tc.expected.Temperature) + assertF32Eq(t, "humidity", res.Humidity, tc.expected.Humidity) + assertF32Eq(t, "pressure", res.Pressure, tc.expected.Pressure) + assertF32Eq(t, "acceleration x", res.Acceleration[0], tc.expected.Acceleration[0]) + assertF32Eq(t, "acceleration y", res.Acceleration[1], tc.expected.Acceleration[1]) + assertF32Eq(t, "acceleration z", res.Acceleration[2], tc.expected.Acceleration[2]) + assertF32Eq(t, "tx power", res.TxPower, tc.expected.TxPower) + assertF32Eq(t, "battery voltage", res.BatteryVoltage, tc.expected.BatteryVoltage) + assertUintEq(t, "movement counted", res.MovementCounter, tc.expected.MovementCounter) + assertUintEq(t, "measurement sequence number", res.MeasurementSeqNum, tc.expected.MeasurementSeqNum) + if res.Mac.String() != tc.expected.Mac.String() { + t.Errorf("MAC not equal: value %s, expected %s", macToStr(res.Mac), macToStr(tc.expected.Mac)) + } + }) + } +} + +func TestParsingInvalid(t *testing.T) { + cases := []struct { + name string + data []byte + }{ + {"empty", []byte{}}, + {"unsupported format", toB("537FFF")}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := Parse(tc.data) + if err == nil { + t.Fatalf("Expected error, got success") + } + }) + } +} diff --git a/pkg/sinks/BUILD.bazel b/pkg/sinks/BUILD.bazel new file mode 100644 index 0000000..58b4675 --- /dev/null +++ b/pkg/sinks/BUILD.bazel @@ -0,0 +1,36 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "gcp_sink.go", + "mqtt_sink.go", + "ratelimiter.go", + "sinks.go", + "stdout_sink.go", + ], + importpath = "github.com/p2004a/gbcsdpd/pkg/sinks", + visibility = ["//visibility:public"], + deps = [ + "//api:go_default_library", + "//pkg/backoff:go_default_library", + "//pkg/config:go_default_library", + "@com_github_dgrijalva_jwt_go//:go_default_library", + "@com_github_eclipse_paho_mqtt_golang//:go_default_library", + "@com_github_golang_protobuf//jsonpb:go_default_library_gen", + "@com_github_golang_protobuf//proto:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["mqtt_sink_test.go"], + embed = [":go_default_library"], + deps = [ + "//api:go_default_library", + "//pkg/config:go_default_library", + "@com_github_eclipse_paho_mqtt_golang//:go_default_library", + "@com_github_fhmq_hmq//broker:go_default_library", + "@com_github_google_go_cmp//cmp:go_default_library", + ], +) diff --git a/pkg/sinks/gcp_sink.go b/pkg/sinks/gcp_sink.go new file mode 100644 index 0000000..740fabd --- /dev/null +++ b/pkg/sinks/gcp_sink.go @@ -0,0 +1,55 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sinks + +import ( + "fmt" + "log" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/p2004a/gbcsdpd/pkg/config" +) + +// NewGCPSink creates new GCPSink. +func NewGCPSink(c *config.GCPSink) (*MQTTSink, error) { + clientID := fmt.Sprintf("projects/%s/locations/%s/registries/%s/devices/%s", c.Project, c.Region, c.Registry, c.Device) + creds := func() (username string, password string) { + username = "unused" + + claims := &jwt.StandardClaims{ + Audience: c.Project, + IssuedAt: time.Now().Unix(), + ExpiresAt: time.Now().Add(20 * time.Minute).Unix(), + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + password, err := token.SignedString(c.Key) + if err != nil { + log.Fatalf("Failed to sign token: %v", err) + } + return + } + mqttClient, err := createMQTTClient(clientID, c.ServerName, c.ServerPort, c.TLSConfig, creds) + if err != nil { + return nil, fmt.Errorf("failed to create MQTT client: %v", err) + } + s := &MQTTSink{ + mqttClient: mqttClient, + topic: fmt.Sprintf("/devices/%s/events/v1", c.Device), + format: config.BINARY, + } + s.rl = newRateLimiter(c.RateLimit, s.groupPublish) + return s, nil +} diff --git a/pkg/sinks/mqtt_sink.go b/pkg/sinks/mqtt_sink.go new file mode 100644 index 0000000..cc4b7ad --- /dev/null +++ b/pkg/sinks/mqtt_sink.go @@ -0,0 +1,115 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sinks + +import ( + "crypto/tls" + "fmt" + "log" + "time" + + MQTT "github.com/eclipse/paho.mqtt.golang" + "github.com/golang/protobuf/jsonpb" + "github.com/golang/protobuf/proto" + api "github.com/p2004a/gbcsdpd/api" + "github.com/p2004a/gbcsdpd/pkg/backoff" + "github.com/p2004a/gbcsdpd/pkg/config" +) + +func connectMQTTClientWithBackoff(client MQTT.Client) { + for retryNum := 0; !client.IsConnected(); retryNum++ { + time.Sleep(backoff.Exponential(retryNum, time.Second, time.Minute*5, 2.0)) + token := client.Connect() + token.Wait() + if token.Error() != nil { + log.Printf("Failed to connect: %v, retrying...", token.Error()) + } + } +} + +func createMQTTClient(clientID, server string, port int, tls *tls.Config, creds MQTT.CredentialsProvider) (MQTT.Client, error) { + opts := MQTT.NewClientOptions() + opts.SetClientID(clientID) + opts.SetKeepAlive(time.Minute) + opts.SetProtocolVersion(4) // MQTT 3.1.1 + opts.SetCleanSession(true) + var brokerAddr string + if tls != nil { + opts.SetTLSConfig(tls) + brokerAddr = fmt.Sprintf("tls://%s:%d", server, port) + } else { + brokerAddr = fmt.Sprintf("tcp://%s:%d", server, port) + } + opts.AddBroker(brokerAddr) + opts.SetCredentialsProvider(creds) + opts.SetConnectionLostHandler(func(client MQTT.Client, err error) { + log.Printf("Disconnected %s (%v), reconnecting...", brokerAddr, err) + connectMQTTClientWithBackoff(client) + }) + + client := MQTT.NewClient(opts) + connectMQTTClientWithBackoff(client) + return client, nil +} + +// MQTTSink publishes measurements to MQTT. +type MQTTSink struct { + mqttClient MQTT.Client + rl *rateLimiter + topic string + format config.PublicationFormat +} + +// Publish is used to push measurement for publication. +func (s *MQTTSink) Publish(m *api.Measurement) { + s.rl.Publish(m) +} + +func (s *MQTTSink) groupPublish(ms []*api.Measurement) { + pub := &api.MeasurementsPublication{Measurements: ms} + if s.format == config.BINARY { + serPub, err := proto.Marshal(pub) + if err != nil { + log.Fatalf("Failed to binary encode measurement: %v", err) + } + s.mqttClient.Publish(s.topic, 0, false, serPub) + } else if s.format == config.JSON { + jsonPub, err := (&jsonpb.Marshaler{}).MarshalToString(pub) + if err != nil { + log.Fatalf("Failed to json encode measurement: %v", err) + } + s.mqttClient.Publish(s.topic, 0, false, jsonPub) + } else { + log.Fatalf("Unknown data publication format: %v", s.format) + } +} + +// NewMQTTSink creates new MQTTSink. +func NewMQTTSink(c *config.MQTTSink) (*MQTTSink, error) { + creds := func() (string, string) { + return c.UserName, c.Password + } + mqttClient, err := createMQTTClient(c.ClientID, c.ServerName, c.ServerPort, c.TLSConfig, creds) + if err != nil { + return nil, fmt.Errorf("failed to create MQTT client: %v", err) + } + s := &MQTTSink{ + mqttClient: mqttClient, + topic: c.Topic, + format: c.Format, + } + s.rl = newRateLimiter(c.RateLimit, s.groupPublish) + return s, nil +} diff --git a/pkg/sinks/mqtt_sink_test.go b/pkg/sinks/mqtt_sink_test.go new file mode 100644 index 0000000..6c5de48 --- /dev/null +++ b/pkg/sinks/mqtt_sink_test.go @@ -0,0 +1,184 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sinks + +import ( + "encoding/json" + "fmt" + "net" + "strings" + "testing" + "time" + + MQTT "github.com/eclipse/paho.mqtt.golang" + "github.com/fhmq/hmq/broker" + "github.com/google/go-cmp/cmp" + api "github.com/p2004a/gbcsdpd/api" + "github.com/p2004a/gbcsdpd/pkg/config" +) + +const testerClientID = "testerclient" + +func pickFreePort() int { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + panic(err) + } + port := listener.Addr().(*net.TCPAddr).Port + err = listener.Close() + if err != nil { + panic(err) + } + return port +} + +// Implements fhmq/hmq/plugins/auth Auth interface +type singleUserAuth struct { + UserName, Password, ClientID string +} + +func (a *singleUserAuth) CheckConnect(clientID, username, password string) bool { + return (a.ClientID == clientID && a.UserName == username && a.Password == password) || clientID == testerClientID +} + +func (a *singleUserAuth) CheckACL(action, clientID, username, ip, topic string) bool { + return true +} + +type msg struct { + Topic string + Payload []byte +} + +func subscribeToAllTopics(t *testing.T, address string) <-chan msg { + opts := MQTT.NewClientOptions() + opts.SetClientID(testerClientID) + opts.SetProtocolVersion(4) // MQTT 3.1.1 + opts.AddBroker(address) + opts.SetAutoReconnect(true) + opts.SetConnectRetry(true) + opts.SetConnectRetryInterval(time.Millisecond * 5) + opts.SetCleanSession(false) + opts.SetResumeSubs(true) + + client := MQTT.NewClient(opts) + + if token := client.Connect(); token.Wait() && token.Error() != nil { + t.Fatalf("failed to connect to hmq broker: %v", token.Error()) + } + + sub := make(chan msg, 20) + messageHandler := func(c MQTT.Client, m MQTT.Message) { + sub <- msg{Topic: m.Topic(), Payload: m.Payload()} + m.Ack() + } + if token := client.Subscribe("#", 1, messageHandler); token.Wait() && token.Error() != nil { + t.Fatalf("failed to subscribe to hmq broker: %v", token.Error()) + } + return sub +} + +func TestBorkerIntegration(t *testing.T) { + testClientID := "pusher" + testUserName := "bob" + testPassword := "ilovealice" + sensorMac := "01:23:45:67:89:AB" + measuementsTopic := "/measurements" + + // Create and start MQTT broker. + port := pickFreePort() + b, err := broker.NewBroker(&broker.Config{ + Worker: 1, + Host: "127.0.0.1", + Port: fmt.Sprintf("%d", port), + Plugin: broker.Plugins{ + Auth: &singleUserAuth{ + UserName: testUserName, + Password: testPassword, + ClientID: testClientID, + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create broker: %v", err) + } + b.Start() + + brokerAddr := fmt.Sprintf("tcp://127.0.0.1:%d", port) + + sink, err := NewMQTTSink(&config.MQTTSink{ + Name: "sink", + Topic: measuementsTopic, + ClientID: testClientID, + UserName: testUserName, + Password: testPassword, + Format: config.JSON, + ServerName: "127.0.0.1", + ServerPort: port, + }) + if err != nil { + t.Fatalf("Failed to create mqtt sink: %v", err) + } + + go func() { + for { + time.Sleep(100 * time.Microsecond) + sink.Publish(&api.Measurement{ + SensorMac: sensorMac, + Temperature: 10.0, + Humidity: 60.0, + }) + } + }() + + count := 0 + for msg := range subscribeToAllTopics(t, brokerAddr) { + // Skip all the broker system messages. + if strings.HasPrefix(msg.Topic, "$") { + continue + } + count++ + if count == 10 { + break + } + if msg.Topic != measuementsTopic { + t.Errorf("Received message on wrong topic. got: %s expected: %s", msg.Topic, measuementsTopic) + } + + type Measurement struct { + Mac string `json:"sensorMac"` + Temp float32 `json:"temperature"` + Humid float32 `json:"humidity"` + } + type Payload struct { + Measurements []Measurement `json:"measurements"` + } + var payload Payload + + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + t.Errorf("failed to unmarshal json message: %v", err) + continue + } + if diff := cmp.Diff(payload, Payload{ + Measurements: []Measurement{{ + Mac: sensorMac, + Temp: 10.0, + Humid: 60.0, + }}, + }); diff != "" { + t.Errorf("unexpected difference in received message:\n%v", diff) + } + } +} diff --git a/pkg/sinks/ratelimiter.go b/pkg/sinks/ratelimiter.go new file mode 100644 index 0000000..83dc42a --- /dev/null +++ b/pkg/sinks/ratelimiter.go @@ -0,0 +1,79 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sinks + +import ( + "math/rand" + "time" + + api "github.com/p2004a/gbcsdpd/api" + "github.com/p2004a/gbcsdpd/pkg/config" +) + +type groupPublish func([]*api.Measurement) + +type rateLimiter struct { + config *config.RateLimit + measurements chan *api.Measurement + cb groupPublish +} + +func (rl *rateLimiter) Publish(m *api.Measurement) { + if rl.config != nil { + rl.measurements <- m + } else { + rl.cb([]*api.Measurement{m}) + } +} + +func (rl *rateLimiter) nextWaitDuration() time.Duration { + d := float64(rl.config.Max1In) + jitter := (rand.Float64()*0.4 - 0.2) * d + return time.Duration(d + jitter) +} + +func (rl *rateLimiter) limiter() { + deadline := time.After(rl.nextWaitDuration()) + mset := make(map[string]*api.Measurement) + for { + select { + case m := <-rl.measurements: + mset[m.SensorMac] = m + case <-deadline: + if len(mset) > 0 { + var ms []*api.Measurement + for _, m := range mset { + ms = append(ms, m) + } + rl.cb(ms) + mset = make(map[string]*api.Measurement) + } + deadline = time.After(rl.nextWaitDuration()) + } + } +} + +// Config can be nil, then the limier will basically copy requests to cb +func newRateLimiter(config *config.RateLimit, cb groupPublish) *rateLimiter { + rl := &rateLimiter{ + config: config, + measurements: make(chan *api.Measurement, 4), + cb: cb, + } + if rl.config != nil { + go rl.limiter() + } + return rl +} diff --git a/pkg/sinks/sinks.go b/pkg/sinks/sinks.go new file mode 100644 index 0000000..b956795 --- /dev/null +++ b/pkg/sinks/sinks.go @@ -0,0 +1,41 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sinks + +import ( + "fmt" + + api "github.com/p2004a/gbcsdpd/api" + "github.com/p2004a/gbcsdpd/pkg/config" +) + +// Sink represents an object with Publish method for publishing api.Measurement. +type Sink interface { + Publish(*api.Measurement) +} + +// NewSink creates a new Sink objects based on the config.Sink configuration. +func NewSink(sinkConfig config.Sink) (Sink, error) { + switch s := sinkConfig.(type) { + case *config.GCPSink: + return NewGCPSink(s) + case *config.StdoutSink: + return NewStdoutSink(s) + case *config.MQTTSink: + return NewMQTTSink(s) + default: + return nil, fmt.Errorf("unknown sink config type: %v", sinkConfig) + } +} diff --git a/pkg/sinks/stdout_sink.go b/pkg/sinks/stdout_sink.go new file mode 100644 index 0000000..86eef1e --- /dev/null +++ b/pkg/sinks/stdout_sink.go @@ -0,0 +1,55 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sinks + +import ( + "fmt" + "sort" + + api "github.com/p2004a/gbcsdpd/api" + "github.com/p2004a/gbcsdpd/pkg/config" +) + +// StdoutSink publishes measurements on standard output. +type StdoutSink struct { + config *config.StdoutSink + rl *rateLimiter +} + +// Publish is used to push measurement for publication. +func (s *StdoutSink) Publish(m *api.Measurement) { + s.rl.Publish(m) +} + +type byMac []*api.Measurement + +func (a byMac) Len() int { return len(a) } +func (a byMac) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byMac) Less(i, j int) bool { return a[i].SensorMac < a[j].SensorMac } + +func (s *StdoutSink) groupPublish(ms []*api.Measurement) { + sort.Sort(byMac(ms)) + for _, m := range ms { + fmt.Printf("[%s] %s = %2.2f°C, %3.2f%%, %4.2fhPa, %1.2fV\n", s.config.Name, + m.SensorMac, m.Temperature, m.Humidity, m.Pressure, m.BatteryVoltage) + } +} + +// NewStdoutSink creates new StdoutSink. +func NewStdoutSink(config *config.StdoutSink) (*StdoutSink, error) { + s := &StdoutSink{config: config} + s.rl = newRateLimiter(config.RateLimit, s.groupPublish) + return s, nil +} diff --git a/repositories.bzl b/repositories.bzl new file mode 100644 index 0000000..671b11d --- /dev/null +++ b/repositories.bzl @@ -0,0 +1,759 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module contains go repositoried managed by gazelle: +$ bazel run //:gazelle -- update-repos -from_file=go.mod -to_macro=repositories.bzl%go_repositories -prune +""" + +load("@bazel_gazelle//:deps.bzl", "go_repository") + +def go_repositories(): + """Fetches all go repositories required by project.""" + go_repository( + name = "co_honnef_go_tools", + importpath = "honnef.co/go/tools", + sum = "h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=", + version = "v0.0.1-2020.1.4", + ) + go_repository( + name = "com_github_bitly_go_simplejson", + importpath = "github.com/bitly/go-simplejson", + sum = "h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=", + version = "v0.5.0", + ) + go_repository( + name = "com_github_bmizerany_assert", + importpath = "github.com/bmizerany/assert", + sum = "h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=", + version = "v0.0.0-20160611221934-b7ed37b82869", + ) + + go_repository( + name = "com_github_burntsushi_toml", + importpath = "github.com/BurntSushi/toml", + sum = "h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=", + version = "v0.3.1", + ) + go_repository( + name = "com_github_burntsushi_xgb", + importpath = "github.com/BurntSushi/xgb", + sum = "h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=", + version = "v0.0.0-20160522181843-27f122750802", + ) + + go_repository( + name = "com_github_census_instrumentation_opencensus_proto", + importpath = "github.com/census-instrumentation/opencensus-proto", + sum = "h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=", + version = "v0.2.1", + ) + go_repository( + name = "com_github_chzyer_logex", + importpath = "github.com/chzyer/logex", + sum = "h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=", + version = "v1.1.10", + ) + go_repository( + name = "com_github_chzyer_readline", + importpath = "github.com/chzyer/readline", + sum = "h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=", + version = "v0.0.0-20180603132655-2972be24d48e", + ) + go_repository( + name = "com_github_chzyer_test", + importpath = "github.com/chzyer/test", + sum = "h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=", + version = "v0.0.0-20180213035817-a1ea475d72b1", + ) + + go_repository( + name = "com_github_client9_misspell", + importpath = "github.com/client9/misspell", + sum = "h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=", + version = "v0.3.4", + ) + go_repository( + name = "com_github_cncf_udpa_go", + importpath = "github.com/cncf/udpa/go", + sum = "h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M=", + version = "v0.0.0-20201120205902-5459f2c99403", + ) + go_repository( + name = "com_github_datadog_zstd", + importpath = "github.com/DataDog/zstd", + sum = "h1:2T/jmrHeTezcCM58lvEQXs0UpQJCo5SoGAcg+mbSTIg=", + version = "v1.3.6-0.20190409195224-796139022798", + ) + + go_repository( + name = "com_github_davecgh_go_spew", + importpath = "github.com/davecgh/go-spew", + sum = "h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=", + version = "v1.1.1", + ) + go_repository( + name = "com_github_dgrijalva_jwt_go", + importpath = "github.com/dgrijalva/jwt-go", + sum = "h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=", + version = "v3.2.0+incompatible", + ) + go_repository( + name = "com_github_eapache_go_resiliency", + importpath = "github.com/eapache/go-resiliency", + sum = "h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU=", + version = "v1.1.0", + ) + go_repository( + name = "com_github_eapache_go_xerial_snappy", + importpath = "github.com/eapache/go-xerial-snappy", + sum = "h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=", + version = "v0.0.0-20180814174437-776d5712da21", + ) + go_repository( + name = "com_github_eapache_queue", + importpath = "github.com/eapache/queue", + sum = "h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=", + version = "v1.1.0", + ) + + go_repository( + name = "com_github_eclipse_paho_mqtt_golang", + importpath = "github.com/eclipse/paho.mqtt.golang", + sum = "h1:/sS2PA+PgomTO1bfJSDJncox+U7X5Boa3AfhEywYdgI=", + version = "v1.3.4", + ) + go_repository( + name = "com_github_envoyproxy_go_control_plane", + importpath = "github.com/envoyproxy/go-control-plane", + sum = "h1:QyzYnTnPE15SQyUeqU6qLbWxMkwyAyu+vGksa0b7j00=", + version = "v0.9.9-0.20210217033140-668b12f5399d", + ) + go_repository( + name = "com_github_envoyproxy_protoc_gen_validate", + importpath = "github.com/envoyproxy/protoc-gen-validate", + sum = "h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=", + version = "v0.1.0", + ) + go_repository( + name = "com_github_fhmq_hmq", + importpath = "github.com/fhmq/hmq", + sum = "h1:bLEsIK6oQ95esJ3uusPCiUXYI8qd2QtbdaQjJeGvpI0=", + version = "v0.0.0-20210318020249-ccbe364f9fbe", + ) + go_repository( + name = "com_github_gin_contrib_sse", + importpath = "github.com/gin-contrib/sse", + sum = "h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=", + version = "v0.0.0-20190301062529-5545eab6dad3", + ) + go_repository( + name = "com_github_gin_gonic_gin", + importpath = "github.com/gin-gonic/gin", + sum = "h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=", + version = "v1.4.0", + ) + + go_repository( + name = "com_github_go_gl_glfw", + importpath = "github.com/go-gl/glfw", + sum = "h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=", + version = "v0.0.0-20190409004039-e6da0acd62b1", + ) + go_repository( + name = "com_github_go_gl_glfw_v3_3_glfw", + importpath = "github.com/go-gl/glfw/v3.3/glfw", + sum = "h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=", + version = "v0.0.0-20200222043503-6f7a984d4dc4", + ) + + go_repository( + name = "com_github_godbus_dbus_v5", + importpath = "github.com/godbus/dbus/v5", + sum = "h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=", + version = "v5.0.4", + ) + go_repository( + name = "com_github_golang_glog", + importpath = "github.com/golang/glog", + sum = "h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=", + version = "v0.0.0-20160126235308-23def4e6c14b", + ) + go_repository( + name = "com_github_golang_groupcache", + importpath = "github.com/golang/groupcache", + sum = "h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=", + version = "v0.0.0-20200121045136-8c9f03a8e57e", + ) + + go_repository( + name = "com_github_golang_mock", + importpath = "github.com/golang/mock", + sum = "h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=", + version = "v1.5.0", + ) + go_repository( + name = "com_github_golang_protobuf", + importpath = "github.com/golang/protobuf", + sum = "h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=", + version = "v1.5.2", + ) + go_repository( + name = "com_github_golang_snappy", + importpath = "github.com/golang/snappy", + sum = "h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=", + version = "v0.0.1", + ) + + go_repository( + name = "com_github_google_btree", + importpath = "github.com/google/btree", + sum = "h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=", + version = "v1.0.0", + ) + + go_repository( + name = "com_github_google_go_cmp", + importpath = "github.com/google/go-cmp", + sum = "h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=", + version = "v0.5.5", + ) + go_repository( + name = "com_github_google_martian", + importpath = "github.com/google/martian", + sum = "h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=", + version = "v2.1.0+incompatible", + ) + go_repository( + name = "com_github_google_martian_v3", + importpath = "github.com/google/martian/v3", + sum = "h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60=", + version = "v3.1.0", + ) + go_repository( + name = "com_github_google_pprof", + importpath = "github.com/google/pprof", + sum = "h1:zIaiqGYDQwa4HVx5wGRTXbx38Pqxjemn4BP98wpzpXo=", + version = "v0.0.0-20210226084205-cbba55b83ad5", + ) + go_repository( + name = "com_github_google_renameio", + importpath = "github.com/google/renameio", + sum = "h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=", + version = "v0.1.0", + ) + go_repository( + name = "com_github_google_uuid", + importpath = "github.com/google/uuid", + sum = "h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=", + version = "v1.1.2", + ) + + go_repository( + name = "com_github_googleapis_gax_go_v2", + importpath = "github.com/googleapis/gax-go/v2", + sum = "h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=", + version = "v2.0.5", + ) + go_repository( + name = "com_github_gorilla_websocket", + importpath = "github.com/gorilla/websocket", + sum = "h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=", + version = "v1.4.2", + ) + go_repository( + name = "com_github_hashicorp_go_uuid", + importpath = "github.com/hashicorp/go-uuid", + sum = "h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=", + version = "v1.0.1", + ) + + go_repository( + name = "com_github_hashicorp_golang_lru", + importpath = "github.com/hashicorp/golang-lru", + sum = "h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=", + version = "v0.5.1", + ) + go_repository( + name = "com_github_ianlancetaylor_demangle", + importpath = "github.com/ianlancetaylor/demangle", + sum = "h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=", + version = "v0.0.0-20200824232613-28f6c0f3b639", + ) + go_repository( + name = "com_github_jcmturner_gofork", + importpath = "github.com/jcmturner/gofork", + sum = "h1:FUwcHNlEqkqLjLBdCp5PRlCFijNjvcYANOZXzCfXwCM=", + version = "v0.0.0-20190328161633-dc7c13fece03", + ) + go_repository( + name = "com_github_json_iterator_go", + importpath = "github.com/json-iterator/go", + sum = "h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=", + version = "v1.1.6", + ) + + go_repository( + name = "com_github_jstemmer_go_junit_report", + importpath = "github.com/jstemmer/go-junit-report", + sum = "h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=", + version = "v0.9.1", + ) + go_repository( + name = "com_github_kisielk_gotool", + importpath = "github.com/kisielk/gotool", + sum = "h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=", + version = "v1.0.0", + ) + go_repository( + name = "com_github_kr_pretty", + importpath = "github.com/kr/pretty", + sum = "h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=", + version = "v0.1.0", + ) + go_repository( + name = "com_github_kr_pty", + importpath = "github.com/kr/pty", + sum = "h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=", + version = "v1.1.1", + ) + go_repository( + name = "com_github_kr_text", + importpath = "github.com/kr/text", + sum = "h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=", + version = "v0.1.0", + ) + go_repository( + name = "com_github_mattn_go_isatty", + importpath = "github.com/mattn/go-isatty", + sum = "h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=", + version = "v0.0.7", + ) + go_repository( + name = "com_github_modern_go_concurrent", + importpath = "github.com/modern-go/concurrent", + sum = "h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=", + version = "v0.0.0-20180306012644-bacd9c7ef1dd", + ) + go_repository( + name = "com_github_modern_go_reflect2", + importpath = "github.com/modern-go/reflect2", + sum = "h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=", + version = "v1.0.1", + ) + go_repository( + name = "com_github_patrickmn_go_cache", + importpath = "github.com/patrickmn/go-cache", + sum = "h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=", + version = "v2.1.0+incompatible", + ) + go_repository( + name = "com_github_pierrec_lz4", + importpath = "github.com/pierrec/lz4", + sum = "h1:GeinFsrjWz97fAxVUEd748aV0cYL+I6k44gFJTCVvpU=", + version = "v0.0.0-20190327172049-315a67e90e41", + ) + go_repository( + name = "com_github_pkg_errors", + importpath = "github.com/pkg/errors", + sum = "h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=", + version = "v0.8.1", + ) + go_repository( + name = "com_github_pkg_profile", + importpath = "github.com/pkg/profile", + sum = "h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=", + version = "v1.2.1", + ) + + go_repository( + name = "com_github_pmezard_go_difflib", + importpath = "github.com/pmezard/go-difflib", + sum = "h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=", + version = "v1.0.0", + ) + + # Moved to WORKSPACE + # go_repository( + # name = "com_github_pelletier_go_toml", + # importpath = "github.com/pelletier/go-toml", + # sum = "h1:NOd0BRdOKpPf0SxkL3HxSQOG7rNh+4kl6PHcBPFs7Q0=", + # version = "v1.9.0", + # ) + go_repository( + name = "com_github_prometheus_client_model", + importpath = "github.com/prometheus/client_model", + sum = "h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=", + version = "v0.0.0-20190812154241-14fe0d1b01d4", + ) + go_repository( + name = "com_github_rcrowley_go_metrics", + importpath = "github.com/rcrowley/go-metrics", + sum = "h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ=", + version = "v0.0.0-20181016184325-3113b8401b8a", + ) + + go_repository( + name = "com_github_rogpeppe_go_internal", + importpath = "github.com/rogpeppe/go-internal", + sum = "h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=", + version = "v1.3.0", + ) + go_repository( + name = "com_github_segmentio_fasthash", + importpath = "github.com/segmentio/fasthash", + sum = "h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ=", + version = "v0.0.0-20180216231524-a72b379d632e", + ) + go_repository( + name = "com_github_shopify_sarama", + importpath = "github.com/Shopify/sarama", + sum = "h1:slvlbm7bxyp7sKQbUwha5BQdZTqurhRoI+zbKorVigQ=", + version = "v1.23.0", + ) + go_repository( + name = "com_github_shopify_toxiproxy", + importpath = "github.com/Shopify/toxiproxy", + sum = "h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=", + version = "v2.1.4+incompatible", + ) + + go_repository( + name = "com_github_stretchr_objx", + importpath = "github.com/stretchr/objx", + sum = "h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=", + version = "v0.1.0", + ) + go_repository( + name = "com_github_stretchr_testify", + importpath = "github.com/stretchr/testify", + sum = "h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=", + version = "v1.6.1", + ) + go_repository( + name = "com_github_tidwall_gjson", + importpath = "github.com/tidwall/gjson", + sum = "h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w=", + version = "v1.6.8", + ) + go_repository( + name = "com_github_tidwall_match", + importpath = "github.com/tidwall/match", + sum = "h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=", + version = "v1.0.3", + ) + go_repository( + name = "com_github_tidwall_pretty", + importpath = "github.com/tidwall/pretty", + sum = "h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=", + version = "v1.0.2", + ) + go_repository( + name = "com_github_ugorji_go", + importpath = "github.com/ugorji/go", + sum = "h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=", + version = "v1.1.4", + ) + go_repository( + name = "com_github_xdg_scram", + importpath = "github.com/xdg/scram", + sum = "h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=", + version = "v0.0.0-20180814205039-7eeb5667e42c", + ) + go_repository( + name = "com_github_xdg_stringprep", + importpath = "github.com/xdg/stringprep", + sum = "h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=", + version = "v1.0.0", + ) + + go_repository( + name = "com_github_yuin_goldmark", + importpath = "github.com/yuin/goldmark", + sum = "h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=", + version = "v1.2.1", + ) + + go_repository( + name = "com_google_cloud_go", + importpath = "cloud.google.com/go", + sum = "h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8=", + version = "v0.81.0", + ) + go_repository( + name = "com_google_cloud_go_bigquery", + importpath = "cloud.google.com/go/bigquery", + sum = "h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA=", + version = "v1.8.0", + ) + go_repository( + name = "com_google_cloud_go_datastore", + importpath = "cloud.google.com/go/datastore", + sum = "h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=", + version = "v1.1.0", + ) + go_repository( + name = "com_google_cloud_go_pubsub", + importpath = "cloud.google.com/go/pubsub", + sum = "h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU=", + version = "v1.3.1", + ) + go_repository( + name = "com_google_cloud_go_storage", + importpath = "cloud.google.com/go/storage", + sum = "h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA=", + version = "v1.10.0", + ) + go_repository( + name = "com_shuralyov_dmitri_gpu_mtl", + importpath = "dmitri.shuralyov.com/gpu/mtl", + sum = "h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=", + version = "v0.0.0-20190408044501-666a987793e9", + ) + go_repository( + name = "in_gopkg_check_v1", + importpath = "gopkg.in/check.v1", + sum = "h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=", + version = "v1.0.0-20180628173108-788fd7840127", + ) + go_repository( + name = "in_gopkg_errgo_v2", + importpath = "gopkg.in/errgo.v2", + sum = "h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=", + version = "v2.1.0", + ) + go_repository( + name = "in_gopkg_go_playground_assert_v1", + importpath = "gopkg.in/go-playground/assert.v1", + sum = "h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=", + version = "v1.2.1", + ) + go_repository( + name = "in_gopkg_go_playground_validator_v8", + importpath = "gopkg.in/go-playground/validator.v8", + sum = "h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=", + version = "v8.18.2", + ) + go_repository( + name = "in_gopkg_jcmturner_aescts_v1", + importpath = "gopkg.in/jcmturner/aescts.v1", + sum = "h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw=", + version = "v1.0.1", + ) + go_repository( + name = "in_gopkg_jcmturner_dnsutils_v1", + importpath = "gopkg.in/jcmturner/dnsutils.v1", + sum = "h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM=", + version = "v1.0.1", + ) + go_repository( + name = "in_gopkg_jcmturner_goidentity_v3", + importpath = "gopkg.in/jcmturner/goidentity.v3", + sum = "h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI=", + version = "v3.0.0", + ) + go_repository( + name = "in_gopkg_jcmturner_gokrb5_v7", + importpath = "gopkg.in/jcmturner/gokrb5.v7", + sum = "h1:hHMV/yKPwMnJhPuPx7pH2Uw/3Qyf+thJYlisUc44010=", + version = "v7.2.3", + ) + go_repository( + name = "in_gopkg_jcmturner_rpc_v1", + importpath = "gopkg.in/jcmturner/rpc.v1", + sum = "h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU=", + version = "v1.1.0", + ) + + go_repository( + name = "in_gopkg_yaml_v2", + importpath = "gopkg.in/yaml.v2", + sum = "h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=", + version = "v2.2.2", + ) + go_repository( + name = "in_gopkg_yaml_v3", + importpath = "gopkg.in/yaml.v3", + sum = "h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=", + version = "v3.0.0-20200313102051-9f266ea9e77c", + ) + + go_repository( + name = "io_opencensus_go", + importpath = "go.opencensus.io", + sum = "h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=", + version = "v0.23.0", + ) + go_repository( + name = "io_rsc_binaryregexp", + importpath = "rsc.io/binaryregexp", + sum = "h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=", + version = "v0.2.0", + ) + go_repository( + name = "io_rsc_quote_v3", + importpath = "rsc.io/quote/v3", + sum = "h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=", + version = "v3.1.0", + ) + go_repository( + name = "io_rsc_sampler", + importpath = "rsc.io/sampler", + sum = "h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=", + version = "v1.3.0", + ) + go_repository( + name = "org_golang_google_api", + importpath = "google.golang.org/api", + sum = "h1:jkDWHOBIoNSD0OQpq4rtBVu+Rh325MPjXG1rakAp8JU=", + version = "v0.46.0", + ) + + go_repository( + name = "org_golang_google_appengine", + importpath = "google.golang.org/appengine", + sum = "h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=", + version = "v1.6.7", + ) + go_repository( + name = "org_golang_google_genproto", + importpath = "google.golang.org/genproto", + sum = "h1:tzkHckzMzgPr8SC4taTC3AldLr4+oJivSoq1xf/nhsc=", + version = "v0.0.0-20210510173355-fb37daa5cd7a", + ) + go_repository( + name = "org_golang_google_grpc", + build_file_proto_mode = "disable", + importpath = "google.golang.org/grpc", + sum = "h1:ARnQJNWxGyYJpdf/JXscNlQr/uv607ZPU9Z7ogHi+iI=", + version = "v1.37.1", + ) + go_repository( + name = "org_golang_google_protobuf", + importpath = "google.golang.org/protobuf", + sum = "h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=", + version = "v1.26.0", + ) + go_repository( + name = "org_golang_x_crypto", + importpath = "golang.org/x/crypto", + sum = "h1:N6Jp/LCiEoIBX56BZSR2bepK5GtbSC2DDOYT742mMfE=", + version = "v0.0.0-20210513122933-cd7d49e622d5", + ) + go_repository( + name = "org_golang_x_exp", + importpath = "golang.org/x/exp", + sum = "h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y=", + version = "v0.0.0-20200224162631-6cc2880d07d6", + ) + go_repository( + name = "org_golang_x_image", + importpath = "golang.org/x/image", + sum = "h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=", + version = "v0.0.0-20190802002840-cff245a6509b", + ) + + go_repository( + name = "org_golang_x_lint", + importpath = "golang.org/x/lint", + sum = "h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=", + version = "v0.0.0-20201208152925-83fdc39ff7b5", + ) + go_repository( + name = "org_golang_x_mobile", + importpath = "golang.org/x/mobile", + sum = "h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=", + version = "v0.0.0-20190719004257-d2bd2a29d028", + ) + go_repository( + name = "org_golang_x_mod", + importpath = "golang.org/x/mod", + sum = "h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY=", + version = "v0.4.1", + ) + + go_repository( + name = "org_golang_x_net", + importpath = "golang.org/x/net", + sum = "h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=", + version = "v0.0.0-20210510120150-4163338589ed", + ) + go_repository( + name = "org_golang_x_oauth2", + importpath = "golang.org/x/oauth2", + sum = "h1:SgVl/sCtkicsS7psKkje4H9YtjdEl3xsYh7N+5TDHqY=", + version = "v0.0.0-20210427180440-81ed05c6b58c", + ) + go_repository( + name = "org_golang_x_sync", + importpath = "golang.org/x/sync", + sum = "h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=", + version = "v0.0.0-20210220032951-036812b2e83c", + ) + go_repository( + name = "org_golang_x_sys", + importpath = "golang.org/x/sys", + sum = "h1:yhBbb4IRs2HS9PPlAg6DMC6mUOKexJBNsLf4Z+6En1Q=", + version = "v0.0.0-20210511113859-b0526f3d8744", + ) + go_repository( + name = "org_golang_x_term", + importpath = "golang.org/x/term", + sum = "h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=", + version = "v0.0.0-20201126162022-7de9c90e9dd1", + ) + + go_repository( + name = "org_golang_x_text", + importpath = "golang.org/x/text", + sum = "h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=", + version = "v0.3.6", + ) + go_repository( + name = "org_golang_x_time", + importpath = "golang.org/x/time", + sum = "h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=", + version = "v0.0.0-20191024005414-555d28b269f0", + ) + + go_repository( + name = "org_golang_x_tools", + importpath = "golang.org/x/tools", + sum = "h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=", + version = "v0.1.0", + ) + go_repository( + name = "org_golang_x_xerrors", + importpath = "golang.org/x/xerrors", + sum = "h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=", + version = "v0.0.0-20200804184101-5ec99f83aff1", + ) + go_repository( + name = "org_uber_go_atomic", + importpath = "go.uber.org/atomic", + sum = "h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=", + version = "v1.4.0", + ) + go_repository( + name = "org_uber_go_multierr", + importpath = "go.uber.org/multierr", + sum = "h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=", + version = "v1.1.0", + ) + go_repository( + name = "org_uber_go_zap", + importpath = "go.uber.org/zap", + sum = "h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=", + version = "v1.10.0", + ) diff --git a/scripts/regen_go_proto.sh b/scripts/regen_go_proto.sh new file mode 100755 index 0000000..3d2d53f --- /dev/null +++ b/scripts/regen_go_proto.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +TEST=false +while getopts "t" opt; do + case "${opt}" in + t) + TEST=true + ;; + *) + exit 1 + ;; + esac +done +cd "$(bazel info workspace)" +bazel build //api/... +files=($(bazel aquery 'kind(proto, //api/...)' | grep Outputs | grep "[.]pb[.]go" | sed 's/Outputs: \[//' | sed 's/\]//' | tr "," "\n")) +for src in ${files[@]}; +do + dst="$(echo $src | sed -E 's|.*/github.com/p2004a/gbcsdpd/(.*)|\1|')" + if [[ $TEST = true ]]; then + diff -u "$src" "$dst" + else + echo "copying $dst" + cp --no-preserve=mode,ownership "$src" "$dst" + fi +done