From b1b617cde91f5dc23769b5f1c294788ca98e453e Mon Sep 17 00:00:00 2001 From: Daniel Adam Date: Mon, 8 Jul 2024 16:12:45 +0200 Subject: [PATCH 1/2] Introduce snippet-service --- .github/workflows/build-publish.yaml | 4 + .github/workflows/test.yml | 10 +- .vscode/settings.json | 2 + Makefile | 21 +- bundle/Dockerfile | 11 + bundle/nginx/nginx.conf.template | 14 + bundle/run.sh | 56 + .../templates/http-gateway/_helpers.tpl | 14 + .../templates/http-gateway/config.yaml | 1 + .../templates/snippet-service/_helpers.tpl | 53 + .../templates/snippet-service/config.yaml | 112 + .../templates/snippet-service/deployment.yaml | 129 + .../templates/snippet-service/domain-crt.yaml | 34 + .../snippet-service/grpc-ingress.yaml | 40 + .../snippet-service/grpc-service.yaml | 25 + .../snippet-service/http-ingress.yaml | 40 + .../snippet-service/http-service.yaml | 25 + .../templates/snippet-service/role.yaml | 3 + .../snippet-service/service-account.yaml | 9 + .../snippet-service/service-crt.yaml | 44 + charts/plgd-hub/values.yaml | 248 ++ cloud2cloud-gateway/service/retrieveDevice.go | 31 +- cloud2cloud-gateway/test/test.go | 6 +- coap-gateway/service/clientObserveHandler.go | 4 +- coap-gateway/service/clientSubscriber.go | 13 + .../service/observation/deviceObserver.go | 4 +- coap-gateway/service/resourceDirectory.go | 2 + coap-gateway/service/signIn.go | 4 +- coap-gateway/test/test.go | 2 +- go.mod | 4 +- go.sum | 4 + grpc-gateway/client/client_test.go | 21 - grpc-gateway/client/createResource_test.go | 3 +- grpc-gateway/client/deleteResource_test.go | 5 +- grpc-gateway/client/deviceSubscriber.go | 8 + .../client/deviceSubscriptions_test.go | 7 +- grpc-gateway/client/getDevice_test.go | 3 +- grpc-gateway/client/getDevices_test.go | 3 +- grpc-gateway/client/getResource_test.go | 3 +- grpc-gateway/client/maintenance.go | 4 +- grpc-gateway/client/maintenance_test.go | 5 +- .../client/observeDeviceResources_test.go | 3 +- grpc-gateway/client/observeDevices_test.go | 3 +- grpc-gateway/client/observeResource_test.go | 3 +- grpc-gateway/client/updateResource_test.go | 5 +- grpc-gateway/pb/README.md | 8 + grpc-gateway/pb/createResource.go | 9 + grpc-gateway/pb/deleteResource.go | 9 + grpc-gateway/pb/devices.pb.go | 714 +++--- grpc-gateway/pb/devices.proto | 6 + grpc-gateway/pb/doc.html | 56 + grpc-gateway/pb/getPendingCommands.pb.go | 118 +- grpc-gateway/pb/getPendingCommands.proto | 1 + grpc-gateway/pb/hubConfiguration.pb.go | 122 +- grpc-gateway/pb/hubConfiguration.proto | 2 + grpc-gateway/pb/service.swagger.json | 53 + grpc-gateway/pb/updateResource.go | 9 + grpc-gateway/service/createResource.go | 71 +- grpc-gateway/service/deleteResource.go | 28 +- .../service/subscribeToEvents_test.go | 21 +- grpc-gateway/service/updateResource.go | 29 +- grpc-gateway/service/updateResource_test.go | 238 ++ grpc-gateway/test/test.go | 21 + http-gateway/config.yaml | 1 + http-gateway/service/config.go | 1 + .../service/getDevicePendingCommands_test.go | 21 +- .../service/getDeviceResourceLinks_test.go | 6 +- http-gateway/service/getHubConfiguration.go | 1 + .../service/getHubConfiguration_test.go | 2 + .../service/getPendingCommands_test.go | 21 +- .../getResourcePendingCommands_test.go | 21 +- http-gateway/test/test.go | 3 +- http-gateway/uri/uri.go | 72 +- .../src/containers/PendingCommands/hooks.ts | 2 +- identity-store/service/service_test.go | 2 +- m2m-oauth-server/test/test.go | 2 +- pkg/mongodb/marshal.go | 89 + pkg/mongodb/mongo.go | 14 + pkg/net/grpc/server/newServer.go | 19 +- pkg/net/http/service/service.go | 8 + pkg/service/service.go | 1 + pkg/strings/slice.go | 28 +- pkg/strings/slice_test.go | 43 - resource-aggregate/commands/commands.pb.go | 726 +++--- resource-aggregate/commands/resouces.go | 23 + resource-aggregate/commands/utils.go | 36 + resource-aggregate/commands/utils_test.go | 108 +- .../cqrs/eventstore/cqldb/load.go | 4 +- .../cqrs/eventstore/mongodb/load.go | 13 +- .../cqrs/projection/projection.go | 8 + .../events/resourceStateSnapshotTaken.go | 137 +- ...esourceStateSnapshotTaken_internal_test.go | 49 + .../events/resourceStateSnapshotTaken_test.go | 550 +++++ resource-aggregate/events/resourceUpdated.go | 16 + resource-aggregate/pb/commands.proto | 3 + resource-aggregate/service/aggregate.go | 39 +- resource-aggregate/service/grpcApi.go | 31 +- resource-directory/service/deviceDirectory.go | 2 +- .../service/getPendingCommands_test.go | 21 +- resource-directory/service/projection.go | 63 +- .../service/resourceLinksProjection.go | 2 +- .../service/resourceProjection.go | 24 + resource-directory/service/resourceShadow.go | 15 +- snippet-service/Makefile | 64 + snippet-service/cmd/service/main.go | 48 + snippet-service/config.yaml | 107 + snippet-service/jq/jq.go | 23 + snippet-service/jq/jq_test.go | 129 + snippet-service/pb/README.md | 424 ++++ snippet-service/pb/appliedConfiguration.go | 144 ++ snippet-service/pb/condition.go | 59 + snippet-service/pb/configuration.go | 70 + snippet-service/pb/doc.html | 1604 +++++++++++++ snippet-service/pb/idfilter.go | 271 +++ snippet-service/pb/idfilter_test.go | 157 ++ snippet-service/pb/service.pb.go | 2091 +++++++++++++++++ snippet-service/pb/service.pb.gw.go | 1017 ++++++++ snippet-service/pb/service.proto | 284 +++ snippet-service/pb/service.swagger.json | 997 ++++++++ snippet-service/pb/service_grpc.pb.go | 567 +++++ snippet-service/pb/tags.go | 28 + snippet-service/service/config.go | 111 + snippet-service/service/config_test.go | 257 ++ snippet-service/service/grpc/config.go | 7 + snippet-service/service/grpc/server.go | 349 +++ snippet-service/service/grpc/service.go | 35 + snippet-service/service/http/config.go | 13 + .../service/http/createCondition_test.go | 154 ++ .../service/http/createConfiguration_test.go | 205 ++ .../http/deleteAppliedConfigurations_test.go | 153 ++ .../service/http/deleteConditions_test.go | 147 ++ .../service/http/deleteConfigurations_test.go | 147 ++ .../http/getAppliedConfigurations_test.go | 180 ++ .../service/http/getConditions_test.go | 158 ++ .../service/http/getConfigurations_test.go | 163 ++ .../service/http/invokeConfiguration_test.go | 414 ++++ .../service/http/requestHandler.go | 79 + snippet-service/service/http/service.go | 48 + .../service/http/updateCondition_test.go | 190 ++ .../service/http/updateConfiguration_test.go | 194 ++ snippet-service/service/http/uri.go | 34 + snippet-service/service/resourceSubscriber.go | 75 + .../service/resourceSubscriber_test.go | 101 + snippet-service/service/service.go | 158 ++ snippet-service/service/service_test.go | 631 +++++ snippet-service/store/appliedConfiguration.go | 75 + snippet-service/store/condition.go | 144 ++ snippet-service/store/config/config.go | 9 + snippet-service/store/configuration.go | 116 + .../store/cqldb/appliedConfiguration.go | 32 + snippet-service/store/cqldb/condition.go | 32 + snippet-service/store/cqldb/config.go | 15 + snippet-service/store/cqldb/configuration.go | 32 + snippet-service/store/cqldb/store.go | 9 + .../store/mongodb/appliedConfiguration.go | 309 +++ snippet-service/store/mongodb/condition.go | 234 ++ snippet-service/store/mongodb/config.go | 13 + .../store/mongodb/configuration.go | 151 ++ .../createAppliedConfiguration_test.go | 324 +++ .../store/mongodb/createCondition_test.go | 134 ++ .../store/mongodb/createConfiguration_test.go | 157 ++ .../deleteAppliedConfiguration_test.go | 151 ++ .../store/mongodb/deleteConditions_test.go | 357 +++ .../mongodb/deleteConfigurations_test.go | 359 +++ .../mongodb/getAppliedConfigurations_test.go | 440 ++++ .../store/mongodb/getConditions_test.go | 413 ++++ .../store/mongodb/getConfigurations_test.go | 345 +++ .../store/mongodb/getLatestConditions_test.go | 309 +++ snippet-service/store/mongodb/store.go | 77 + .../updateAppliedConfiguration_test.go | 152 ++ .../store/mongodb/updateCondition_test.go | 160 ++ .../store/mongodb/updateConfiguration_test.go | 123 + snippet-service/store/mongodb/utility.go | 229 ++ snippet-service/store/store.go | 109 + snippet-service/test/appliedConfiguration.go | 160 ++ snippet-service/test/condition.go | 150 ++ snippet-service/test/configuration.go | 139 ++ snippet-service/test/service.go | 158 ++ snippet-service/test/test.go | 297 +++ snippet-service/updater/config.go | 42 + snippet-service/updater/config_test.go | 58 + snippet-service/updater/expiredUpdates.go | 23 + snippet-service/updater/resourceUpdater.go | 729 ++++++ .../updater/resourceUpdater_test.go | 112 + snippet-service/updater/updateExecution.go | 73 + snippet-service/uri/uri.go | 7 + test/config/config.go | 24 +- test/http/request.go | 24 + test/http/uri.go | 10 +- test/iotivity-lite/service/offboard_test.go | 4 +- test/iotivity-lite/service/republish_test.go | 2 +- test/pb/pendingCommand.go | 21 +- test/test.go | 4 +- 193 files changed, 22711 insertions(+), 1210 deletions(-) create mode 100644 charts/plgd-hub/templates/snippet-service/_helpers.tpl create mode 100644 charts/plgd-hub/templates/snippet-service/config.yaml create mode 100644 charts/plgd-hub/templates/snippet-service/deployment.yaml create mode 100644 charts/plgd-hub/templates/snippet-service/domain-crt.yaml create mode 100644 charts/plgd-hub/templates/snippet-service/grpc-ingress.yaml create mode 100644 charts/plgd-hub/templates/snippet-service/grpc-service.yaml create mode 100644 charts/plgd-hub/templates/snippet-service/http-ingress.yaml create mode 100644 charts/plgd-hub/templates/snippet-service/http-service.yaml create mode 100644 charts/plgd-hub/templates/snippet-service/role.yaml create mode 100644 charts/plgd-hub/templates/snippet-service/service-account.yaml create mode 100644 charts/plgd-hub/templates/snippet-service/service-crt.yaml create mode 100644 pkg/mongodb/marshal.go create mode 100644 pkg/mongodb/mongo.go create mode 100644 resource-aggregate/events/resourceStateSnapshotTaken_internal_test.go create mode 100644 snippet-service/Makefile create mode 100644 snippet-service/cmd/service/main.go create mode 100644 snippet-service/config.yaml create mode 100644 snippet-service/jq/jq.go create mode 100644 snippet-service/jq/jq_test.go create mode 100644 snippet-service/pb/README.md create mode 100644 snippet-service/pb/appliedConfiguration.go create mode 100644 snippet-service/pb/condition.go create mode 100644 snippet-service/pb/configuration.go create mode 100644 snippet-service/pb/doc.html create mode 100644 snippet-service/pb/idfilter.go create mode 100644 snippet-service/pb/idfilter_test.go create mode 100644 snippet-service/pb/service.pb.go create mode 100644 snippet-service/pb/service.pb.gw.go create mode 100644 snippet-service/pb/service.proto create mode 100644 snippet-service/pb/service.swagger.json create mode 100644 snippet-service/pb/service_grpc.pb.go create mode 100644 snippet-service/pb/tags.go create mode 100644 snippet-service/service/config.go create mode 100644 snippet-service/service/config_test.go create mode 100644 snippet-service/service/grpc/config.go create mode 100644 snippet-service/service/grpc/server.go create mode 100644 snippet-service/service/grpc/service.go create mode 100644 snippet-service/service/http/config.go create mode 100644 snippet-service/service/http/createCondition_test.go create mode 100644 snippet-service/service/http/createConfiguration_test.go create mode 100644 snippet-service/service/http/deleteAppliedConfigurations_test.go create mode 100644 snippet-service/service/http/deleteConditions_test.go create mode 100644 snippet-service/service/http/deleteConfigurations_test.go create mode 100644 snippet-service/service/http/getAppliedConfigurations_test.go create mode 100644 snippet-service/service/http/getConditions_test.go create mode 100644 snippet-service/service/http/getConfigurations_test.go create mode 100644 snippet-service/service/http/invokeConfiguration_test.go create mode 100644 snippet-service/service/http/requestHandler.go create mode 100644 snippet-service/service/http/service.go create mode 100644 snippet-service/service/http/updateCondition_test.go create mode 100644 snippet-service/service/http/updateConfiguration_test.go create mode 100644 snippet-service/service/http/uri.go create mode 100644 snippet-service/service/resourceSubscriber.go create mode 100644 snippet-service/service/resourceSubscriber_test.go create mode 100644 snippet-service/service/service.go create mode 100644 snippet-service/service/service_test.go create mode 100644 snippet-service/store/appliedConfiguration.go create mode 100644 snippet-service/store/condition.go create mode 100644 snippet-service/store/config/config.go create mode 100644 snippet-service/store/configuration.go create mode 100644 snippet-service/store/cqldb/appliedConfiguration.go create mode 100644 snippet-service/store/cqldb/condition.go create mode 100644 snippet-service/store/cqldb/config.go create mode 100644 snippet-service/store/cqldb/configuration.go create mode 100644 snippet-service/store/cqldb/store.go create mode 100644 snippet-service/store/mongodb/appliedConfiguration.go create mode 100644 snippet-service/store/mongodb/condition.go create mode 100644 snippet-service/store/mongodb/config.go create mode 100644 snippet-service/store/mongodb/configuration.go create mode 100644 snippet-service/store/mongodb/createAppliedConfiguration_test.go create mode 100644 snippet-service/store/mongodb/createCondition_test.go create mode 100644 snippet-service/store/mongodb/createConfiguration_test.go create mode 100644 snippet-service/store/mongodb/deleteAppliedConfiguration_test.go create mode 100644 snippet-service/store/mongodb/deleteConditions_test.go create mode 100644 snippet-service/store/mongodb/deleteConfigurations_test.go create mode 100644 snippet-service/store/mongodb/getAppliedConfigurations_test.go create mode 100644 snippet-service/store/mongodb/getConditions_test.go create mode 100644 snippet-service/store/mongodb/getConfigurations_test.go create mode 100644 snippet-service/store/mongodb/getLatestConditions_test.go create mode 100644 snippet-service/store/mongodb/store.go create mode 100644 snippet-service/store/mongodb/updateAppliedConfiguration_test.go create mode 100644 snippet-service/store/mongodb/updateCondition_test.go create mode 100644 snippet-service/store/mongodb/updateConfiguration_test.go create mode 100644 snippet-service/store/mongodb/utility.go create mode 100644 snippet-service/store/store.go create mode 100644 snippet-service/test/appliedConfiguration.go create mode 100644 snippet-service/test/condition.go create mode 100644 snippet-service/test/configuration.go create mode 100644 snippet-service/test/service.go create mode 100644 snippet-service/test/test.go create mode 100644 snippet-service/updater/config.go create mode 100644 snippet-service/updater/config_test.go create mode 100644 snippet-service/updater/expiredUpdates.go create mode 100644 snippet-service/updater/resourceUpdater.go create mode 100644 snippet-service/updater/resourceUpdater_test.go create mode 100644 snippet-service/updater/updateExecution.go create mode 100644 snippet-service/uri/uri.go diff --git a/.github/workflows/build-publish.yaml b/.github/workflows/build-publish.yaml index db8d2f36a..1483fc143 100644 --- a/.github/workflows/build-publish.yaml +++ b/.github/workflows/build-publish.yaml @@ -109,6 +109,10 @@ jobs: - name: cert-tool directory: tools/cert-tool file: tools/cert-tool/Dockerfile + - name: snippet-service + directory: snippet-service + file: .tmp/docker/snippet-service/Dockerfile + template-file: tools/docker/Dockerfile.in - name: mongodb-standby-tool directory: tools/mongodb/standby-tool file: tools/mongodb/standby-tool/Dockerfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c7139d79..79a19f626 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: log: level: "debug" dumpBody: "true" - + - name: test/cqldb cmd: test checkRace: "true" @@ -43,7 +43,6 @@ jobs: log: level: "debug" dumpBody: "true" - # test without check race - name: test/norace @@ -52,7 +51,7 @@ jobs: log: level: "debug" dumpBody: "true" - + - name: test/norace/cqldb cmd: test database: "cqldb" @@ -85,6 +84,10 @@ jobs: log: level: "debug" dumpBody: "true" + snippetService: + log: + level: "debug" + dumpBody: "true" # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -113,6 +116,7 @@ jobs: TEST_RESOURCE_AGGREGATE_LOG_LEVEL=${{ matrix.resourceAggregate.log.level }} TEST_RESOURCE_AGGREGATE_LOG_DUMP_BODY=${{ matrix.resourceAggregate.log.dumpBody }} \ TEST_GRPC_GATEWAY_LOG_LEVEL=${{ matrix.grpcGateway.log.level }} TEST_GRPC_GATEWAY_LOG_DUMP_BODY=${{ matrix.grpcGateway.log.dumpBody }} \ TEST_IDENTITY_STORE_LOG_LEVEL=${{ matrix.identityStore.log.level }} TEST_IDENTITY_STORE_LOG_DUMP_BODY=${{ matrix.identityStore.log.dumpBody }} \ + TEST_SNIPPET_SERVICE_LOG_LEVEL=${{ matrix.snippetService.log.level }} TEST_SNIPPET_SERVICE_LOG_DUMP_BODY=${{ matrix.snippetService.log.dumpBody }} \ TEST_DATABASE=${{ matrix.database }} \ ${{ matrix.args }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 9207a2c6c..2b6faccea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -35,6 +35,8 @@ "TEST_GRPC_GATEWAY_LOG_DUMP_BODY": "false", "TEST_IDENTITY_STORE_LOG_LEVEL": "info", "TEST_IDENTITY_STORE_LOG_DUMP_BODY": "false", + "TEST_SNIPPET_SERVICE_LOG_LEVEL": "info", + "TEST_SNIPPET_SERVICE_LOG_DUMP_BODY": "false", "TEST_DATABASE": "mongoDB", "TEST_BRIDGE_DEVICE_CONFIG": "${workspaceFolder}/.tmp/bridge/config-test.yaml", // "TEST_DEVICE_NAME": "bridged-device-0", diff --git a/Makefile b/Makefile index 612c32686..ddfe61fc2 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,8 @@ TEST_GRPC_GATEWAY_LOG_LEVEL ?= info TEST_GRPC_GATEWAY_LOG_DUMP_BODY ?= false TEST_IDENTITY_STORE_LOG_LEVEL ?= info TEST_IDENTITY_STORE_LOG_DUMP_BODY ?= false +TEST_SNIPPET_SERVICE_LOG_LEVEL ?= info +TEST_SNIPPET_SERVICE_LOG_DUMP_BODY ?= false TEST_MEMORY_COAP_GATEWAY_NUM_DEVICES ?= 1 TEST_MEMORY_COAP_GATEWAY_NUM_RESOURCES ?= 1 TEST_MEMORY_COAP_GATEWAY_EXPECTED_RSS_IN_MB ?= 50 @@ -42,7 +44,7 @@ CERT_TOOL_SIGN_ALG ?= ECDSA-SHA256 CERT_TOOL_ELLIPTIC_CURVE ?= P256 CERT_TOOL_IMAGE = ghcr.io/plgd-dev/hub/cert-tool:vnext -SUBDIRS := bundle certificate-authority cloud2cloud-connector cloud2cloud-gateway coap-gateway grpc-gateway resource-aggregate resource-directory http-gateway identity-store test/oauth-server tools/cert-tool +SUBDIRS := bundle certificate-authority cloud2cloud-connector cloud2cloud-gateway coap-gateway grpc-gateway resource-aggregate resource-directory http-gateway identity-store snippet-service test/oauth-server tools/cert-tool .PHONY: $(SUBDIRS) push proto/generate clean build test env mongo nats certificates hub-build http-gateway-www simulators default: build @@ -327,9 +329,9 @@ define RUN-TESTS-IN-DIRECTORY COVERAGE_FILE=/coverage/$$(echo $(1) | sed -e "s/[\.\/]//g").coverage.txt ; \ JSON_REPORT_FILE=$(WORKING_DIRECTORY)/.tmp/report/$$(echo $(1) | sed -e "s/[\.\/]//g").report.json ; \ if [ -n "$${JSON_REPORT}" ]; then \ - $(call RUN-DOCKER, go test -timeout=45m -race -p 1 -v $(1)... -covermode=atomic -coverprofile=$${COVERAGE_FILE} -json > "$${JSON_REPORT_FILE}") \ + $(call RUN-DOCKER, /bin/sh -c "$(2) go test -timeout=45m -race -p 1 -v $(1)... -covermode=atomic -coverprofile=$${COVERAGE_FILE} -json > $${JSON_REPORT_FILE}") \ else \ - $(call RUN-DOCKER, go test -timeout=45m -race -p 1 -v $(1)... -covermode=atomic -coverprofile=$${COVERAGE_FILE}) \ + $(call RUN-DOCKER, /bin/sh -c "$(2) go test -timeout=45m -race -p 1 -v $(1)... -covermode=atomic -coverprofile=$${COVERAGE_FILE}") \ fi ; \ EXIT_STATUS=$$? ; \ if [ $${EXIT_STATUS} -ne 0 ]; then \ @@ -372,6 +374,7 @@ define RUN-TESTS-UDP TEST_RESOURCE_AGGREGATE_LEVEL=$(TEST_RESOURCE_AGGREGATE_LEVEL) TEST_RESOURCE_AGGREGATE_LOG_DUMP_BODY=$(TEST_RESOURCE_AGGREGATE_LOG_DUMP_BODY) \ TEST_GRPC_GATEWAY_LOG_LEVEL=$(TEST_GRPC_GATEWAY_LOG_LEVEL) TEST_GRPC_GATEWAY_LOG_DUMP_BODY=$(TEST_GRPC_GATEWAY_LOG_DUMP_BODY) \ TEST_IDENTITY_STORE_LOG_LEVEL=$(TEST_IDENTITY_STORE_LOG_LEVEL) TEST_IDENTITY_STORE_LOG_DUMP_BODY=$(TEST_IDENTITY_STORE_LOG_DUMP_BODY) \ + TEST_SNIPPET_SERVICE_LOG_LEVEL=$(TEST_SNIPPET_SERVICE_LOG_LEVEL) TEST_SNIPPET_SERVICE_LOG_DUMP_BODY=$(TEST_SNIPPET_SERVICE_LOG_DUMP_BODY) \ TEST_DATABASE=$(TEST_DATABASE)) $(call RUN-TESTS,iotivity-lite-dtls,./test/iotivity-lite/service,-timeout=$(TEST_TIMEOUT) $(GO_BUILD_ARG) -p 1 -v -tags=test,\ TEST_COAP_GATEWAY_UDP_ENABLED=$(TEST_COAP_GATEWAY_UDP_ENABLED) \ @@ -379,6 +382,7 @@ define RUN-TESTS-UDP TEST_RESOURCE_AGGREGATE_LEVEL=$(TEST_RESOURCE_AGGREGATE_LEVEL) TEST_RESOURCE_AGGREGATE_LOG_DUMP_BODY=$(TEST_RESOURCE_AGGREGATE_LOG_DUMP_BODY) \ TEST_GRPC_GATEWAY_LOG_LEVEL=$(TEST_GRPC_GATEWAY_LOG_LEVEL) TEST_GRPC_GATEWAY_LOG_DUMP_BODY=$(TEST_GRPC_GATEWAY_LOG_DUMP_BODY) \ TEST_IDENTITY_STORE_LOG_LEVEL=$(TEST_IDENTITY_STORE_LOG_LEVEL) TEST_IDENTITY_STORE_LOG_DUMP_BODY=$(TEST_IDENTITY_STORE_LOG_DUMP_BODY) \ + TEST_SNIPPET_SERVICE_LOG_LEVEL=$(TEST_SNIPPET_SERVICE_LOG_LEVEL) TEST_SNIPPET_SERVICE_LOG_DUMP_BODY=$(TEST_SNIPPET_SERVICE_LOG_DUMP_BODY) \ TEST_DATABASE=$(TEST_DATABASE)) endef @@ -391,6 +395,7 @@ test: env hub-test TEST_RESOURCE_AGGREGATE_LEVEL=$(TEST_RESOURCE_AGGREGATE_LEVEL) TEST_RESOURCE_AGGREGATE_LOG_DUMP_BODY=$(TEST_RESOURCE_AGGREGATE_LOG_DUMP_BODY) \ TEST_GRPC_GATEWAY_LOG_LEVEL=$(TEST_GRPC_GATEWAY_LOG_LEVEL) TEST_GRPC_GATEWAY_LOG_DUMP_BODY=$(TEST_GRPC_GATEWAY_LOG_DUMP_BODY) \ TEST_IDENTITY_STORE_LOG_LEVEL=$(TEST_IDENTITY_STORE_LOG_LEVEL) TEST_IDENTITY_STORE_LOG_DUMP_BODY=$(TEST_IDENTITY_STORE_LOG_DUMP_BODY) \ + TEST_SNIPPET_SERVICE_LOG_LEVEL=$(TEST_SNIPPET_SERVICE_LOG_LEVEL) TEST_SNIPPET_SERVICE_LOG_DUMP_BODY=$(TEST_SNIPPET_SERVICE_LOG_DUMP_BODY) \ TEST_DATABASE=$(TEST_DATABASE)) ifeq ($(TEST_COAP_GATEWAY_UDP_ENABLED),true) @$(call RUN-TESTS-UDP) @@ -411,6 +416,7 @@ test/mem: env/test/mem hub-test TEST_RESOURCE_AGGREGATE_LEVEL=$(TEST_RESOURCE_AGGREGATE_LEVEL) TEST_RESOURCE_AGGREGATE_LOG_DUMP_BODY=$(TEST_RESOURCE_AGGREGATE_LOG_DUMP_BODY) \ TEST_GRPC_GATEWAY_LOG_LEVEL=$(TEST_GRPC_GATEWAY_LOG_LEVEL) TEST_GRPC_GATEWAY_LOG_DUMP_BODY=$(TEST_GRPC_GATEWAY_LOG_DUMP_BODY) \ TEST_IDENTITY_STORE_LOG_LEVEL=$(TEST_IDENTITY_STORE_LOG_LEVEL) TEST_IDENTITY_STORE_LOG_DUMP_BODY=$(TEST_IDENTITY_STORE_LOG_DUMP_BODY)\ + TEST_SNIPPET_SERVICE_LOG_LEVEL=$(TEST_SNIPPET_SERVICE_LOG_LEVEL) TEST_SNIPPET_SERVICE_LOG_DUMP_BODY=$(TEST_SNIPPET_SERVICE_LOG_DUMP_BODY) \ TEST_DATABASE=$(TEST_DATABASE)) .PHONY: test/mem @@ -428,7 +434,14 @@ $(test-targets): %: env hub-test echo "No golang files detected, directory $$TARGET_DIRECTORY skipped"; \ exit 0; \ fi ; \ - $(call RUN-TESTS-IN-DIRECTORY,$(patsubst test-%,./%/,$@)) + $(call RUN-TESTS-IN-DIRECTORY,$(patsubst test-%,./%/,$@),\ + TEST_COAP_GATEWAY_UDP_ENABLED=$(TEST_COAP_GATEWAY_UDP_ENABLED) \ + TEST_COAP_GATEWAY_LOG_LEVEL=$(TEST_COAP_GATEWAY_LOG_LEVEL) TEST_COAP_GATEWAY_LOG_DUMP_BODY=$(TEST_COAP_GATEWAY_LOG_DUMP_BODY) \ + TEST_RESOURCE_AGGREGATE_LEVEL=$(TEST_RESOURCE_AGGREGATE_LEVEL) TEST_RESOURCE_AGGREGATE_LOG_DUMP_BODY=$(TEST_RESOURCE_AGGREGATE_LOG_DUMP_BODY) \ + TEST_GRPC_GATEWAY_LOG_LEVEL=$(TEST_GRPC_GATEWAY_LOG_LEVEL) TEST_GRPC_GATEWAY_LOG_DUMP_BODY=$(TEST_GRPC_GATEWAY_LOG_DUMP_BODY) \ + TEST_IDENTITY_STORE_LOG_LEVEL=$(TEST_IDENTITY_STORE_LOG_LEVEL) TEST_IDENTITY_STORE_LOG_DUMP_BODY=$(TEST_IDENTITY_STORE_LOG_DUMP_BODY) \ + TEST_SNIPPET_SERVICE_LOG_LEVEL=$(TEST_SNIPPET_SERVICE_LOG_LEVEL) TEST_SNIPPET_SERVICE_LOG_DUMP_BODY=$(TEST_SNIPPET_SERVICE_LOG_DUMP_BODY) \ + TEST_DATABASE=$(TEST_DATABASE)) .PHONY: $(test-targets) diff --git a/bundle/Dockerfile b/bundle/Dockerfile index 9cc19ab46..3b9a66e7a 100644 --- a/bundle/Dockerfile +++ b/bundle/Dockerfile @@ -113,6 +113,13 @@ RUN go build \ -o "/go/bin/$tool" \ ./ +#snippet-service +ARG service=snippet-service +WORKDIR $root_directory/$service +RUN go build -ldflags "-linkmode external -extldflags -static -X github.com/plgd-dev/hub/v2/pkg/build.CommitDate=$COMMIT_DATE -X github.com/plgd-dev/hub/v2/pkg/build.CommitHash=$SHORT_COMMIT -X github.com/plgd-dev/hub/v2/pkg/build.BuildDate=$DATE -X github.com/plgd-dev/hub/v2/pkg/build.Version=$VERSION -X github.com/plgd-dev/hub/v2/pkg/build.ReleaseURL=$RELEASE_URL" \ + -o "/go/bin/$service" \ + ./cmd/service + #nats WORKDIR $root_directory RUN apkArch="$(apk --print-arch)"; \ @@ -178,6 +185,8 @@ COPY --from=build /go/bin/cloud2cloud-connector /usr/local/bin/cloud2cloud-conne COPY --from=build /go/src/github.com/plgd-dev/hub/cloud2cloud-connector/config.yaml /configs/cloud2cloud-connector.yaml COPY --from=build /go/src/github.com/plgd-dev/hub/bundle/run.sh /usr/local/bin/run.sh COPY --from=build /go/src/github.com/plgd-dev/hub/bundle/nginx /nginx +COPY --from=build /go/bin/snippet-service /usr/local/bin/snippet-service +COPY --from=build /go/src/github.com/plgd-dev/hub/snippet-service/config.yaml /configs/snippet-service.yaml # install scylla RUN curl -sSf get.scylladb.com/server | sudo bash -s -- --scylla-version 5.2 @@ -232,6 +241,8 @@ ENV NATS_PORT=10001 ENV SCYLLA_SMP=1 ENV SCYLLA_DEVELOPER_MODE=true ENV SCYLLA_PORT=29142 +ENV SNIPPET_SERVICE_PORT=9091 +ENV HTTP_SNIPPET_SERVICE_PORT=9092 ENV M2M_OAUTH_SERVER_PORT=9080 # OAuth diff --git a/bundle/nginx/nginx.conf.template b/bundle/nginx/nginx.conf.template index fbcb44c8d..312c36a9f 100644 --- a/bundle/nginx/nginx.conf.template +++ b/bundle/nginx/nginx.conf.template @@ -121,6 +121,20 @@ http { proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; } + location ~ ^(/snippet-service) { + set $upstream_snippet_service https://127.0.0.1:REPLACE_HTTP_SNIPPET_SERVICE_PORT; + proxy_pass $upstream_snippet_service; + proxy_ssl_certificate /data/certs/internal/endpoint.crt; + proxy_ssl_certificate_key /data/certs/internal/endpoint.key; + proxy_ssl_trusted_certificate /data/certs/root_ca.crt; + proxy_ssl_verify on; + set $cors_headers 'Authority,Method,Path,Scheme,Accept,Accept-Encoding,Accept-Language,Content-Type,Origin,Refer,Sec-Fetch-Dest,Sec-Fetch-Mode,Sec-Fetch-Site,Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,correlation-id'; + include /nginx/cors.conf; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + } location ~ ^/ { set $upstream_http_gateway https://127.0.0.1:REPLACE_HTTP_GATEWAY_PORT; proxy_pass $upstream_http_gateway; diff --git a/bundle/run.sh b/bundle/run.sh index ac423c9fa..d6d944b39 100755 --- a/bundle/run.sh +++ b/bundle/run.sh @@ -15,6 +15,7 @@ export NGINX_PATH="/data/nginx" export JETSTREAM_PATH="/data/jetstream" export CERTIFICATE_AUTHORITY_ADDRESS="localhost:${CERTIFICATE_AUTHORITY_PORT}" +export CERTIFICATE_AUTHORITY_HTTP_ADDRESS="localhost:${HTTP_CERTIFICATE_AUTHORITY_PORT}" export MOCK_OAUTH_SERVER_ADDRESS="localhost:${MOCK_OAUTH_SERVER_PORT}" export RESOURCE_AGGREGATE_ADDRESS="localhost:${RESOURCE_AGGREGATE_PORT}" export RESOURCE_DIRECTORY_ADDRESS="localhost:${RESOURCE_DIRECTORY_PORT}" @@ -23,6 +24,8 @@ export GRPC_GATEWAY_ADDRESS="localhost:${GRPC_GATEWAY_PORT}" export HTTP_GATEWAY_ADDRESS="localhost:${HTTP_GATEWAY_PORT}" export CLOUD2CLOUD_GATEWAY_ADDRESS="localhost:${CLOUD2CLOUD_GATEWAY_PORT}" export CLOUD2CLOUD_CONNECTOR_ADDRESS="localhost:${CLOUD2CLOUD_CONNECTOR_PORT}" +export SNIPPET_SERVICE_ADDRESS="localhost:${SNIPPET_SERVICE_PORT}" +export SNIPPET_SERVICE_HTTP_ADDRESS="localhost:${HTTP_SNIPPET_SERVICE_PORT}" export M2M_OAUTH_SERVER_ADDRESS="localhost:${M2M_OAUTH_SERVER_PORT}" export INTERNAL_CERT_DIR_PATH="$CERTIFICATES_PATH/internal" @@ -484,6 +487,7 @@ if [ "${OVERRIDE_FILES}" = "true" ] || [ ! -f "${NGINX_PATH}/nginx.conf" ]; then sed -i "s/REPLACE_CLOUD2CLOUD_GATEWAY_PORT/$CLOUD2CLOUD_GATEWAY_PORT/g" ${NGINX_PATH}/nginx.conf sed -i "s/REPLACE_CLOUD2CLOUD_CONNECTOR_PORT/$CLOUD2CLOUD_CONNECTOR_PORT/g" ${NGINX_PATH}/nginx.conf sed -i "s/REPLACE_HTTP_CERTIFICATE_AUTHORITY_PORT/$HTTP_CERTIFICATE_AUTHORITY_PORT/g" ${NGINX_PATH}/nginx.conf + sed -i "s/REPLACE_HTTP_SNIPPET_SERVICE_PORT/$HTTP_SNIPPET_SERVICE_PORT/g" ${NGINX_PATH}/nginx.conf sed -i "s/REPLACE_M2M_OAUTH_SERVER_PORT/$M2M_OAUTH_SERVER_PORT/g" ${NGINX_PATH}/nginx.conf fi @@ -1009,6 +1013,7 @@ cat /configs/certificate-authority.yaml | yq e "\ .apis.grpc.authorization.http.tls.useSystemCAPool = true | .apis.grpc.authorization.authority = \"https://${OAUTH_ENDPOINT}\" | .apis.grpc.authorization.ownerClaim = \"${OWNER_CLAIM}\" | + .apis.http.address = \"${CERTIFICATE_AUTHORITY_HTTP_ADDRESS}\" | .clients.storage.use = \"${DATABASE_USE}\" | .clients.storage.mongoDB.uri = \"${MONGODB_URI}\" | .clients.storage.cqlDB.hosts = [ \"${SCYLLA_HOSTNAME}\" ] | @@ -1241,7 +1246,51 @@ while true; do sleep 1 done +# snippet-service +echo "starting snippet-service" +## configuration +if [ "${OVERRIDE_FILES}" = "true" ] || [ ! -f "/data/snippet-service.yaml" ]; then +cat /configs/snippet-service.yaml | yq e "\ + .hubID = \"${HUB_ID}\" | + .log.level = \"${LOG_LEVEL}\" | + .apis.grpc.address = \"${SNIPPET_SERVICE_ADDRESS}\" | + .apis.grpc.authorization.audience = \"${SERVICE_OAUTH_AUDIENCE}\" | + .apis.grpc.authorization.http.tls.useSystemCAPool = true | + .apis.grpc.authorization.authority = \"https://${OAUTH_ENDPOINT}\" | + .apis.grpc.authorization.ownerClaim = \"${OWNER_CLAIM}\" | + .apis.http.address = \"${SNIPPET_SERVICE_HTTP_ADDRESS}\" | + .clients.storage.use = \"${DATABASE_USE}\" | + .clients.storage.mongoDB.uri = \"${MONGODB_URI}\" | + .clients.storage.cqlDB.hosts = [ \"${SCYLLA_HOSTNAME}\" ] | + .clients.storage.cqlDB.port = ${SCYLLA_PORT} | + .clients.openTelemetryCollector.grpc.enabled = ${OPEN_TELEMETRY_EXPORTER_ENABLED} | + .clients.openTelemetryCollector.grpc.address = \"${OPEN_TELEMETRY_EXPORTER_ADDRESS}\" | + .clients.openTelemetryCollector.grpc.tls.caPool = \"${OPEN_TELEMETRY_EXPORTER_CA_POOL}\" | + .clients.openTelemetryCollector.grpc.tls.keyFile = \"${OPEN_TELEMETRY_EXPORTER_KEY_FILE}\" | + .clients.openTelemetryCollector.grpc.tls.certFile = \"${OPEN_TELEMETRY_EXPORTER_CERT_FILE}\" | + .clients.openTelemetryCollector.grpc.tls.useSystemCAPool = true +" - > /data/snippet-service.yaml +fi +snippet-service --config /data/snippet-service.yaml >$LOGS_PATH/snippet-service.log 2>&1 & +status=$? +snippet_service_pid=$! +if [ $status -ne 0 ]; then + echo "Failed to start snippet-service: $status" + sync + cat $LOGS_PATH/snippet-service.log + exit $status +fi +# waiting for ca. Without wait, sometimes the service didn't connect. +i=0 +while true; do + i=$((i+1)) + if openssl s_client -connect ${SNIPPET_SERVICE_ADDRESS} -cert ${INTERNAL_CERT_DIR_PATH}/${GRPC_INTERNAL_CERT_NAME} -key ${INTERNAL_CERT_DIR_PATH}/${GRPC_INTERNAL_CERT_KEY_NAME} <<< "Q" 2>/dev/null > /dev/null; then + break + fi + echo "Try to reconnect to snippet-service(${SNIPPET_SERVICE_ADDRESS}) $i" + sleep 1 +done echo "Open browser at https://${DOMAIN}" @@ -1367,4 +1416,11 @@ while sleep 10; do exit 1 fi fi + ps aux |grep $snippet_service_pid |grep -q -v grep + if [ $? -ne 0 ]; then + echo "snippet-service has already exited." + sync + cat $LOGS_PATH/snippet-service.log + exit 1 + fi done diff --git a/charts/plgd-hub/templates/http-gateway/_helpers.tpl b/charts/plgd-hub/templates/http-gateway/_helpers.tpl index e8c19c261..f3e42db4b 100644 --- a/charts/plgd-hub/templates/http-gateway/_helpers.tpl +++ b/charts/plgd-hub/templates/http-gateway/_helpers.tpl @@ -83,6 +83,20 @@ {{- end }} {{- end }} +{{- define "plgd-hub.httpgateway.snippetServiceApiDomain" -}} + {{- $domain := "" }} + {{- if .Values.snippetservice }} + {{- if .Values.snippetservice.enabled }} + {{- if .Values.snippetservice.domain }} + {{- $domain = printf "https://%s" .Values.snippetservice.domain }} + {{- else }} + {{- $domain = printf "https://api.%s" .Values.global.domain }} + {{- end }} + {{- end }} + {{- end }} + {{- printf $domain }} +{{- end }} + {{- define "plgd-hub.httpgateway.uiDomain" -}} {{- if .Values.httpgateway.uiDomain }} {{- printf "%s" .Values.httpgateway.uiDomain }} diff --git a/charts/plgd-hub/templates/http-gateway/config.yaml b/charts/plgd-hub/templates/http-gateway/config.yaml index b5daac2db..ca7a94470 100644 --- a/charts/plgd-hub/templates/http-gateway/config.yaml +++ b/charts/plgd-hub/templates/http-gateway/config.yaml @@ -53,6 +53,7 @@ data: webConfiguration: httpGatewayAddress: {{ .ui.webConfiguration.httpGatewayAddress | default ( printf "https://%s" ( include "plgd-hub.httpgateway.apiDomain" $)) | quote }} deviceProvisioningService: {{ .ui.webConfiguration.deviceProvisioningService | default ( include "plgd-hub.httpgateway.dpsApiDomain" $) | quote }} + snippetService: {{ .ui.webConfiguration.snippetService | default ( include "plgd-hub.httpgateway.snippetServiceApiDomain" $) | quote }} {{- if not $.Values.mockoauthserver.enabled }} webOAuthClient: authority: {{ required ".ui.webConfiguration.webOAuthClient.authority or global.authority is required" ( .ui.webConfiguration.webOAuthClient.authority | default $.Values.global.authority ) | quote }} diff --git a/charts/plgd-hub/templates/snippet-service/_helpers.tpl b/charts/plgd-hub/templates/snippet-service/_helpers.tpl new file mode 100644 index 000000000..0d2d07419 --- /dev/null +++ b/charts/plgd-hub/templates/snippet-service/_helpers.tpl @@ -0,0 +1,53 @@ +{{- define "plgd-hub.snippetservice.fullname" -}} +{{- if .Values.snippetservice.fullnameOverride }} +{{- .Values.snippetservice.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Values.snippetservice.name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s-%s" .Release.Name $name .Values.snippetservice.name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{- define "plgd-hub.snippetservice.configName" -}} + {{- $fullName := include "plgd-hub.snippetservice.fullname" . -}} + {{- printf "%s-cfg" $fullName }} +{{- end -}} + +{{- define "plgd-hub.snippetservice.createServiceCertByCm" }} + {{- $serviceTls := .Values.snippetservice.apis.grpc.tls.certFile }} + {{- if $serviceTls }} + {{- printf "" -}} + {{- else }} + {{- printf "true" -}} + {{- end }} +{{- end }} + +{{- define "plgd-hub.snippetservice.domain" -}} + {{- if .Values.snippetservice.domain }} + {{- printf "%s" .Values.snippetservice.domain }} + {{- else }} + {{- printf "api.%s" .Values.global.domain }} + {{- end }} +{{- end }} + +{{- define "plgd-hub.snippetservice.serviceCertName" -}} + {{- $fullName := include "plgd-hub.snippetservice.fullname" . -}} + {{- printf "%s-crt" $fullName -}} +{{- end }} + +{{- define "plgd-hub.snippetservice.domainCertName" -}} + {{- if .Values.snippetservice.ingress.secretName }} + {{- printf "%s" .Values.snippetservice.ingress.secretName -}} + {{- else }} + {{- $fullName := include "plgd-hub.snippetservice.fullname" . -}} + {{- printf "%s-domain-crt" $fullName -}} + {{- end }} +{{- end }} + +{{- define "plgd-hub.snippetservice.selectorLabels" -}} +app.kubernetes.io/name: {{ .Values.snippetservice.name }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} \ No newline at end of file diff --git a/charts/plgd-hub/templates/snippet-service/config.yaml b/charts/plgd-hub/templates/snippet-service/config.yaml new file mode 100644 index 000000000..384f60237 --- /dev/null +++ b/charts/plgd-hub/templates/snippet-service/config.yaml @@ -0,0 +1,112 @@ +{{- if .Values.snippetservice.enabled }} +{{- $cert := "/certs" }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "plgd-hub.snippetservice.configName" . }} + namespace: {{ .Release.Namespace }} +data: + {{ .Values.snippetservice.config.fileName }}: | + {{- with .Values.snippetservice }} + hubID: {{ required "snippetservice.hubId or global.hubId is required" ( .hubId | default $.Values.global.hubId) | quote }} + log: + level: {{ .log.level }} + dumpBody: {{ .log.dumpBody }} + encoding: {{ .log.encoding }} + stacktrace: + enabled: {{ .log.stacktrace.enabled }} + level: {{ .log.stacktrace.level }} + encoderConfig: + timeEncoder: {{ .log.encoderConfig.timeEncoder }} + apis: + grpc: + address: {{ .apis.grpc.address | default (printf "0.0.0.0:%v" .port) | quote }} + sendMsgSize: {{ int64 .apis.grpc.sendMsgSize | default 4194304 }} + recvMsgSize: {{ int64 .apis.grpc.recvMsgSize | default 4194304 }} + enforcementPolicy: + minTime: {{ .apis.grpc.enforcementPolicy.minTime }} + permitWithoutStream: {{ .apis.grpc.enforcementPolicy.permitWithoutStream }} + keepAlive: + # 0s - means infinity + maxConnectionIdle: {{ .apis.grpc.keepAlive.maxConnectionIdle }} + # 0s - means infinity + maxConnectionAge: {{ .apis.grpc.keepAlive.maxConnectionIdle }} + # 0s - means infinity + maxConnectionAgeGrace: {{ .apis.grpc.keepAlive.maxConnectionAgeGrace }} + time: {{ .apis.grpc.keepAlive.maxConnectionIdle }} + timeout: {{ .apis.grpc.keepAlive.maxConnectionIdle }} + tls: + {{- $tls := .apis.grpc.tls }} + {{- include "plgd-hub.certificateConfig" (list $ $tls $cert ) | indent 8 }} + clientCertificateRequired: {{ .apis.grpc.tls.clientCertificateRequired }} + authorization: + {{- $authorization := .apis.grpc.authorization }} + {{- include "plgd-hub.authorizationConfig" (list $ $authorization "snippetservice.apis.grpc.authorization" $cert ) | indent 8 }} + http: + address: {{ .apis.http.address | default (printf "0.0.0.0:%v" .httpPort) | quote }} + readTimeout: {{ .apis.http.readTimeout }} + readHeaderTimeout: {{ .apis.http.readHeaderTimeout }} + writeTimeout: {{ .apis.http.writeTimeout }} + idleTimeout: {{ .apis.http.idleTimeout }} + clients: + eventBus: + subscriptionID: {{ .clients.eventBus.subscriptionID | default "snippet-service" }} + nats: + url: {{ printf " " }}{{- include "plgd-hub.natsUri" (list $ .clients.eventBus.nats.url) | quote }} + pendingLimits: + msgLimit: {{ .clients.eventBus.nats.pendingLimits.msgLimit }} + bytesLimit: {{ printf "%v" .clients.eventBus.nats.pendingLimits.bytesLimit }} + tls: + {{- $natsTls := .clients.eventBus.nats.tls }} + {{- include "plgd-hub.certificateConfig" (list $ $natsTls $cert ) | indent 10 }} + useSystemCAPool: {{ .clients.eventBus.nats.tls.useSystemCAPool }} + storage: + use: {{ include "plgd-hub.useDatabase" (list $ . .clients.storage.use) | quote }} + mongoDB: + uri: {{ include "plgd-hub.mongoDBUri" (list $ .clients.storage.mongoDB.uri ) | quote }} + database: {{ .clients.storage.mongoDB.database }} + maxPoolSize: {{ .clients.storage.mongoDB.maxPoolSize }} + maxConnIdleTime: {{ .clients.storage.mongoDB.maxConnIdleTime }} + tls: + {{- $mongoDbTls := .clients.storage.mongoDB.tls }} + {{- include "plgd-hub.certificateConfig" (list $ $mongoDbTls $cert ) | indent 10 }} + useSystemCAPool: {{ .clients.storage.mongoDB.tls.useSystemCAPool }} + cqlDB: + hosts: + {{- include "plgd-hub.cqlDBHosts" (list $ .clients.storage.cqlDB.hosts ) | indent 8 }} + port: {{ .clients.storage.cqlDB.port | default 9142 }} + table: {{ .clients.storage.cqlDB.table | quote }} + numConnections: {{ .clients.storage.cqlDB.numConnections }} + connectTimeout: {{ .clients.storage.cqlDB.connectTimeout }} + useHostnameResolution: {{ .clients.storage.cqlDB.useHostnameResolution }} + reconnectionPolicy: + constant: + interval: {{ .clients.storage.cqlDB.reconnectionPolicy.constant.interval }} + maxRetries: {{ .clients.storage.cqlDB.reconnectionPolicy.constant.maxRetries }} + keyspace: + name: {{ .clients.storage.cqlDB.keyspace.name }} + create: {{ .clients.storage.cqlDB.keyspace.create }} + replication: + {{- toYaml .clients.storage.cqlDB.keyspace.replication | nindent 14 }} + tls: + {{- $cqlDbTls := .clients.storage.cqlDB.tls }} + {{- include "plgd-hub.certificateConfig" (list $ $cqlDbTls $cert ) | indent 10 }} + useSystemCAPool: {{ .clients.storage.cqlDB.tls.useSystemCAPool }} + resourceUpdater: + cleanUpExpiredUpdates: {{ .clients.resourceUpdater.cleanUpExpiredUpdates | quote }} + grpc: + {{- $resourceUpdater := .clients.resourceUpdater.grpc.address }} + address:{{ printf " " }}{{- include "plgd-hub.resourceAggregateAddress" (list $ $resourceUpdater ) | quote }} + sendMsgSize: {{ int64 .clients.resourceUpdater.grpc.sendMsgSize | default 4194304 }} + recvMsgSize: {{ int64 .clients.resourceUpdater.grpc.recvMsgSize | default 4194304 }} + keepAlive: + time: {{ .clients.resourceUpdater.grpc.keepAlive.time }} + timeout: {{ .clients.resourceUpdater.grpc.keepAlive.timeout }} + permitWithoutStream: {{ .clients.resourceUpdater.grpc.keepAlive.permitWithoutStream }} + tls: + {{- $raClientTls := .clients.resourceUpdater.grpc.tls }} + {{- include "plgd-hub.certificateConfig" (list $ $raClientTls $cert) | indent 10 }} + useSystemCAPool: {{ .clients.resourceUpdater.grpc.tls.useSystemCAPool }} + {{- include "plgd-hub.openTelemetryExporterConfig" (list $ $cert ) | nindent 6 }} + {{- end }} +{{- end }} diff --git a/charts/plgd-hub/templates/snippet-service/deployment.yaml b/charts/plgd-hub/templates/snippet-service/deployment.yaml new file mode 100644 index 000000000..e433aa572 --- /dev/null +++ b/charts/plgd-hub/templates/snippet-service/deployment.yaml @@ -0,0 +1,129 @@ +{{- if .Values.snippetservice.enabled }} +{{- $cert := "/certs" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "plgd-hub.snippetservice.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "plgd-hub.labels" . | nindent 4 }} + {{- with .Values.snippetservice.deploymentLabels }} + {{- . | toYaml | nindent 4 }} + {{- end }} + {{- with .Values.snippetservice.deploymentAnnotations }} + annotations: + {{- . | toYaml | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.snippetservice.replicas }} + selector: + matchLabels: + {{- include "plgd-hub.snippetservice.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.snippetservice.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "plgd-hub.snippetservice.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.snippetservice.securityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.snippetservice.rbac.enabled }} + serviceAccountName: {{ .Values.snippetservice.rbac.serviceAccountName }} + {{- end }} + {{- with .Values.snippetservice.image.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + restartPolicy: {{ .Values.snippetservice.restartPolicy }} + {{- if .Values.snippetservice.initContainersTpl }} + initContainers: + {{- tpl .Values.snippetservice.initContainersTpl . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Values.snippetservice.name }} + {{- with .Values.snippetservice.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: {{ include "plgd-hub.image" (list . .Values.snippetservice) | quote }} + imagePullPolicy: {{ .Values.snippetservice.image.pullPolicy }} + {{- if .Values.snippetservice.command }} + command: + {{- range .Values.snippetservice.command }} + - {{ . | quote }} + {{- end }} + {{- end}} + args: + - "--config" + - {{ printf "%s/%s" .Values.snippetservice.config.mountPath .Values.snippetservice.config.fileName | quote }} + ports: + - name: grpc + containerPort: {{ .Values.snippetservice.port }} + protocol: TCP + - name: http + containerPort: {{ .Values.snippetservice.httpPort }} + protocol: TCP + {{- with .Values.snippetservice.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.snippetservice.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.snippetservice.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: {{ .Values.snippetservice.config.volume }} + mountPath: {{ .Values.snippetservice.config.mountPath }} + {{- if ( include "plgd-hub.snippetservice.createServiceCertByCm" . ) }} + - name: service-crt + mountPath: {{ $cert }} + {{- end }} + {{- if .Values.global.authorizationCAPool }} + - name: {{ .Values.extraAuthorizationCAPool.name }} + mountPath: {{ .Values.extraAuthorizationCAPool.mountPath }} + {{- end }} + {{- with .Values.snippetservice.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.snippetservice.extraContainers }} + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.snippetservice.extraContainers "context" $ ) | nindent 8 }} + {{- end }} + volumes: + - name: {{ .Values.resourcedirectory.config.volume }} + configMap: + name: {{ include "plgd-hub.snippetservice.configName" . }} + {{- if ( include "plgd-hub.snippetservice.createServiceCertByCm" . ) }} + - name: service-crt + secret: + secretName: {{ include "plgd-hub.snippetservice.serviceCertName" . }} + {{- end }} + {{- if .Values.global.authorizationCAPool }} + - name: {{ .Values.extraAuthorizationCAPool.name }} + secret: + secretName: {{ .Values.extraAuthorizationCAPool.name }} + {{- end }} + {{- with .Values.snippetservice.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.snippetservice.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.snippetservice.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.snippetservice.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/plgd-hub/templates/snippet-service/domain-crt.yaml b/charts/plgd-hub/templates/snippet-service/domain-crt.yaml new file mode 100644 index 000000000..c1d06ff62 --- /dev/null +++ b/charts/plgd-hub/templates/snippet-service/domain-crt.yaml @@ -0,0 +1,34 @@ +{{- $domainCrt := include "plgd-hub.snippetservice.domainCertName" . }} +{{- if and $domainCrt .Values.certmanager.enabled .Values.snippetservice.enabled (not $.Values.global.enableWildCartCert ) }} +{{- $serviceDns := include "plgd-hub.snippetservice.fullname" . }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ $domainCrt }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "plgd-hub.labels" . | nindent 4 }} + {{- with .Values.certmanager.external.labels }} + {{- . | toYaml | nindent 4 }} + {{- end }} + {{- with .Values.certmanager.external.annotations }} + annotations: + {{- . | toYaml | nindent 4 }} + {{- end }} +spec: + secretName: {{ $domainCrt }} + privateKey: + algorithm: {{ .Values.certmanager.external.cert.key.algorithm | default .Values.certmanager.default.cert.key.algorithm }} + size: {{ .Values.certmanager.external.cert.key.size | default .Values.certmanager.default.cert.key.size }} + usages: + - server auth + - client auth + dnsNames: + - {{ include "plgd-hub.snippetservice.domain" . | quote }} + duration: {{ .Values.certmanager.external.cert.duration | default .Values.certmanager.default.cert.duration }} + renewBefore: {{ .Values.certmanager.external.cert.renewBefore | default .Values.certmanager.default.cert.renewBefore }} + issuerRef: + name: {{ .Values.certmanager.external.issuer.name | default .Values.certmanager.default.issuer.name }} + kind: {{ .Values.certmanager.external.issuer.kind | default .Values.certmanager.default.issuer.kind }} + group: {{ .Values.certmanager.external.issuer.group | default .Values.certmanager.default.issuer.group }} +{{- end }} diff --git a/charts/plgd-hub/templates/snippet-service/grpc-ingress.yaml b/charts/plgd-hub/templates/snippet-service/grpc-ingress.yaml new file mode 100644 index 000000000..12ef47117 --- /dev/null +++ b/charts/plgd-hub/templates/snippet-service/grpc-ingress.yaml @@ -0,0 +1,40 @@ +{{- if and .Values.snippetservice.enabled .Values.snippetservice.ingress.grpc.enabled }} +{{- $fullname := include "plgd-hub.snippetservice.fullname" . }} +{{- $port := .Values.snippetservice.port }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullname }}-grpc + namespace: {{ .Release.Namespace }} + labels: + {{- include "plgd-hub.labels" . | nindent 4 }} + annotations: + {{- if .Values.snippetservice.ingress.grpc.annotations }} + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.snippetservice.ingress.grpc.annotations "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.snippetservice.ingress.grpc.customAnnotations }} + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.snippetservice.ingress.grpc.customAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + tls: + - hosts: + - {{ include "plgd-hub.snippetservice.domain" . | quote }} + {{- if $.Values.global.enableWildCartCert }} + secretName: {{ include "plgd-hub.wildCardCertName" . | quote }} + {{- else }} + secretName: {{ include "plgd-hub.snippetservice.domainCertName" . | quote }} + {{- end }} + rules: + - host: {{ include "plgd-hub.snippetservice.domain" . | quote }} + http: + paths: + {{- range .Values.snippetservice.ingress.grpc.paths }} + - path: {{ . }} + pathType: Prefix + backend: + service: + name: {{ $fullname }}-grpc + port: + number: {{ $port }} + {{- end }} +{{- end }} diff --git a/charts/plgd-hub/templates/snippet-service/grpc-service.yaml b/charts/plgd-hub/templates/snippet-service/grpc-service.yaml new file mode 100644 index 000000000..e2d37681f --- /dev/null +++ b/charts/plgd-hub/templates/snippet-service/grpc-service.yaml @@ -0,0 +1,25 @@ +{{- if .Values.snippetservice.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "plgd-hub.snippetservice.fullname" . }}-grpc + namespace: {{ .Release.Namespace }} + labels: + {{- include "plgd-hub.labels" . | nindent 4 }} + {{- with .Values.snippetservice.service.grpc.labels }} + {{- . | toYaml | nindent 4 }} + {{- end }} + {{- if .Values.snippetservice.service.grpc.annotations }} + annotations: + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.snippetservice.service.grpc.annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.snippetservice.service.grpc.type | default "ClusterIP" }} + ports: + - port: {{ .Values.snippetservice.port }} + targetPort: {{ .Values.snippetservice.service.grpc.targetPort }} + protocol: {{ .Values.snippetservice.service.grpc.protocol }} + name: {{ .Values.snippetservice.service.grpc.name }} + selector: + {{- include "plgd-hub.snippetservice.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/plgd-hub/templates/snippet-service/http-ingress.yaml b/charts/plgd-hub/templates/snippet-service/http-ingress.yaml new file mode 100644 index 000000000..3ee3cd2b2 --- /dev/null +++ b/charts/plgd-hub/templates/snippet-service/http-ingress.yaml @@ -0,0 +1,40 @@ +{{- if and .Values.snippetservice.enabled .Values.snippetservice.ingress.http.enabled }} +{{- $fullname := include "plgd-hub.snippetservice.fullname" . }} +{{- $port := .Values.snippetservice.httpPort }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullname }}-http + namespace: {{ .Release.Namespace }} + labels: + {{- include "plgd-hub.labels" . | nindent 4 }} + annotations: + {{- if .Values.snippetservice.ingress.http.annotations }} + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.snippetservice.ingress.http.annotations "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.snippetservice.ingress.http.customAnnotations }} + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.snippetservice.ingress.http.customAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + tls: + - hosts: + - {{ include "plgd-hub.snippetservice.domain" . | quote }} + {{- if $.Values.global.enableWildCartCert }} + secretName: {{ include "plgd-hub.wildCardCertName" . | quote }} + {{- else }} + secretName: {{ include "plgd-hub.snippetservice.domainCertName" . | quote }} + {{- end }} + rules: + - host: {{ include "plgd-hub.snippetservice.domain" . | quote }} + http: + paths: + {{- range .Values.snippetservice.ingress.http.paths }} + - path: {{ . }} + pathType: Prefix + backend: + service: + name: {{ $fullname }}-http + port: + number: {{ $port }} + {{- end }} +{{- end }} diff --git a/charts/plgd-hub/templates/snippet-service/http-service.yaml b/charts/plgd-hub/templates/snippet-service/http-service.yaml new file mode 100644 index 000000000..1e6cf659c --- /dev/null +++ b/charts/plgd-hub/templates/snippet-service/http-service.yaml @@ -0,0 +1,25 @@ +{{- if .Values.snippetservice.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "plgd-hub.snippetservice.fullname" . }}-http + namespace: {{ .Release.Namespace }} + labels: + {{- include "plgd-hub.labels" . | nindent 4 }} + {{- with .Values.snippetservice.service.http.labels }} + {{- . | toYaml | nindent 4 }} + {{- end }} + {{- if .Values.snippetservice.service.http.annotations }} + annotations: + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.snippetservice.service.http.annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.snippetservice.service.http.type | default "ClusterIP" }} + ports: + - port: {{ .Values.snippetservice.httpPort }} + targetPort: {{ .Values.snippetservice.service.http.targetPort }} + protocol: {{ .Values.snippetservice.service.http.protocol }} + name: {{ .Values.snippetservice.service.http.name }} + selector: + {{- include "plgd-hub.snippetservice.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/plgd-hub/templates/snippet-service/role.yaml b/charts/plgd-hub/templates/snippet-service/role.yaml new file mode 100644 index 000000000..0dc01b10c --- /dev/null +++ b/charts/plgd-hub/templates/snippet-service/role.yaml @@ -0,0 +1,3 @@ +{{- if and .Values.snippetservice.rbac.enabled .Values.snippetservice.rbac.roleBindingDefitionTpl }} +{{- tpl .Values.snippetservice.rbac.roleBindingDefitionTpl $ }} +{{- end }} diff --git a/charts/plgd-hub/templates/snippet-service/service-account.yaml b/charts/plgd-hub/templates/snippet-service/service-account.yaml new file mode 100644 index 000000000..b1a4a7081 --- /dev/null +++ b/charts/plgd-hub/templates/snippet-service/service-account.yaml @@ -0,0 +1,9 @@ +{{- if and .Values.snippetservice.rbac.enabled .Values.snippetservice.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.snippetservice.rbac.serviceAccountName }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "plgd-hub.labels" . | nindent 4 }} +{{- end }} diff --git a/charts/plgd-hub/templates/snippet-service/service-crt.yaml b/charts/plgd-hub/templates/snippet-service/service-crt.yaml new file mode 100644 index 000000000..afce0d74b --- /dev/null +++ b/charts/plgd-hub/templates/snippet-service/service-crt.yaml @@ -0,0 +1,44 @@ +{{- $createServiceCert := include "plgd-hub.snippetservice.serviceCertName" . }} +{{- if and $createServiceCert .Values.certmanager.enabled .Values.snippetservice.enabled }} +{{- $serviceCertName := include "plgd-hub.snippetservice.serviceCertName" . }} +{{- $serviceDns := include "plgd-hub.snippetservice.fullname" . }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ $serviceCertName }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "plgd-hub.labels" . | nindent 4 }} + {{- with .Values.certmanager.internal.labels }} + {{- . | toYaml | nindent 4 }} + {{- end }} + {{- with .Values.certmanager.internal.annotations }} + annotations: + {{- . | toYaml | nindent 4 }} + {{- end }} +spec: + secretName: {{ $serviceCertName }} + privateKey: + algorithm: {{ .Values.certmanager.internal.cert.key.algorithm | default .Values.certmanager.default.cert.key.algorithm }} + size: {{ .Values.certmanager.internal.cert.key.size | default .Values.certmanager.default.cert.key.size }} + usages: + - server auth + - client auth + dnsNames: + - {{ printf "%s-http.%s.svc.%s" $serviceDns .Release.Namespace .Values.cluster.dns | quote }} + - {{ printf "%s-grpc.%s.svc.%s" $serviceDns .Release.Namespace .Values.cluster.dns | quote }} + - {{ printf "%s-http" $serviceDns | quote }} + - {{ printf "%s-grpc" $serviceDns | quote }} + {{- if .Values.snippetservice.service.grpc.crt.extraDnsNames }} + {{- toYaml .Values.snippetservice.service.grpc.crt.extraDnsNames | nindent 4}} + {{- end }} + {{- if .Values.snippetservice.service.http.crt.extraDnsNames }} + {{- toYaml .Values.snippetservice.service.http.crt.extraDnsNames | nindent 4}} + {{- end }} + duration: {{ .Values.certmanager.internal.cert.duration | default .Values.certmanager.default.cert.duration }} + renewBefore: {{ .Values.certmanager.internal.cert.renewBefore | default .Values.certmanager.default.cert.renewBefore }} + issuerRef: + name: {{ .Values.certmanager.internal.issuer.name | default .Values.certmanager.default.issuer.name }} + kind: {{ .Values.certmanager.internal.issuer.kind | default .Values.certmanager.default.issuer.kind }} + group: {{ .Values.certmanager.internal.issuer.group | default .Values.certmanager.default.issuer.group }} +{{- end }} diff --git a/charts/plgd-hub/values.yaml b/charts/plgd-hub/values.yaml index fe6831bab..082f9e255 100644 --- a/charts/plgd-hub/values.yaml +++ b/charts/plgd-hub/values.yaml @@ -1740,6 +1740,7 @@ httpgateway: webConfiguration: httpGatewayAddress: "" deviceProvisioningService: "" + snippetService: "" webOAuthClient: authority: "" clientID: "" @@ -2292,6 +2293,253 @@ certificateauthority: validFrom: "now-1h" expiresIn: "87600h" +snippetservice: + # -- Enable snippet-service + enabled: true + # -- Name of component. Used in label selectors + name: snippet-service + # -- Full name to override + fullnameOverride: + # -- Number of replicas + replicas: 1 + # -- Additional labels for snippet-service deployment + deploymentLabels: {} + # -- Additional annotations for snippet-service deployment + deploymentAnnotations: {} + # -- Pod security context + podSecurityContext: {} + # -- Labels for snippet-service pod + podLabels: {} + # -- Annotations for snippet-service pod + podAnnotations: {} + service: + grpc: + # -- Service type + type: ClusterIP + # -- Labels for snippet-service + labels: {} + # -- Annotations for snippet-service + annotations: {} + # -- Target port + targetPort: grpc + # -- Protocol + protocol: TCP + # -- Name + name: grpc + crt: + # -- Extra DNS names for service certificate + extraDnsNames: [] + http: + # -- Service type + type: ClusterIP + # -- Labels for snippet service + labels: {} + # -- Annotations for snippet service + annotations: {} + # -- Target port + targetPort: http + # -- Protocol + protocol: TCP + # -- Name + name: http + crt: + # -- Extra DNS names for service certificate + extraDnsNames: [] + # -- RBAC configuration + rbac: + # -- Enable RBAC + enabled: false + # -- Name of snippet service SA + serviceAccountName: snippet-service + # -- Template definition for Role/binding etc.. + roleBindingDefitionTpl: + # -- Security context for pod + securityContext: + # -- Image pull secrets + imagePullSecrets: + # -- Restart policy for pod + restartPolicy: Always + # -- Init containers definition + initContainersTpl: + # -- Extra POD containers + extraContainers: {} + image: + # -- Image registry + registry: ghcr.io/ + # -- Image repository + repository: plgd-dev/hub/snippet-service + # -- Image tag. + tag: + # -- Image pull policy + pullPolicy: Always + # -- Image pull secrets + imagePullSecrets: + # -- Liveness probe. snippet-service doesn't have any default liveness probe + livenessProbe: + # -- Readiness probe. snippet-service doesn't have aby default readiness probe + readinessProbe: + # -- Resources limit + resources: + # -- Node selector + nodeSelector: + # -- Affinity definition + affinity: + # -- Toleration definition + tolerations: + # -- Optional extra volumes + extraVolumes: + # -- Optional extra volume mounts + extraVolumeMounts: + # -- External domain for snippet-service. Default: api.{{ global.domain }} + domain: + ingress: + http: + # -- Enable ingress + enabled: true + # -- Override name of host/tls secret. If not specified, it will be generated + secretName: + # -- Pre defined map of Ingress annotation + annotations: + nginx.org/grpc-services: "{{ include \"plgd-hub.snippetservice.fullname\" . }}-http" + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/enable-cors: "true" + cert-manager.io/private-key-rotation-policy: always + # -- Custom map of Ingress annotation + customAnnotations: {} + # -- Ingress path + paths: + - /snippet-service + grpc: + # -- Enable ingress + enabled: true + # -- Override name of host/tls secret. If not specified, it will be generated + secretName: + # -- Pre defined map of Ingress annotation + annotations: + nginx.org/grpc-services: "{{ include \"plgd-hub.snippetservice.fullname\" . }}-grpc" + nginx.ingress.kubernetes.io/backend-protocol: "GRPCS" + ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/enable-cors: "true" + cert-manager.io/private-key-rotation-policy: always + # -- Custom map of Ingress annotation + customAnnotations: { } + # -- Paths + paths: + - /snippetservice.pb.SnippetService + # -- Service configuration + config: + # -- File name for config file + fileName: service.yaml + # -- Config file volume name + volume: config + # -- Mount path + mountPath: /config + # -- Service and POD port + port: 9100 + httpPort: 9101 + # -- Hub ID. Overrides the global.hubId + hubId: + # -- Log section + log: + # -- Logging enabled from level + level: info + # -- Dump grpc messages + dumpBody: false + # -- The supported values are: "json", "console" + encoding: json + stacktrace: + # -- Log stacktrace + enabled: false + # -- Stacktrace from level + level: warn + encoderConfig: + # -- Time format for logs. The supported values are: "rfc3339nano", "rfc3339" + timeEncoder: rfc3339nano + # -- For complete snippet-service configuration see [plgd/snippet-service](https://github.com/plgd-dev/hub/tree/main/snippet-service) + apis: + grpc: + address: + sendMsgSize: 4194304 + recvMsgSize: 4194304 + enforcementPolicy: + minTime: 5s + permitWithoutStream: true + keepAlive: + # 0s - means infinity + maxConnectionIdle: 0s + # 0s - means infinity + maxConnectionAge: 0s + # 0s - means infinity + maxConnectionAgeGrace: 0s + time: 2h + timeout: 20s + tls: + caPool: + keyFile: + certFile: + clientCertificateRequired: false + authorization: + ownerClaim: + authority: + audience: + http: + maxIdleConns: 16 + maxConnsPerHost: 32 + maxIdleConnsPerHost: 16 + idleConnTimeout: "30s" + timeout: "10s" + tls: + caPool: + keyFile: + certFile: + useSystemCAPool: true + http: + address: + readTimeout: 8s + readHeaderTimeout: 4s + writeTimeout: 16s + idleTimeout: 30s + clients: + eventBus: + subscriptionID: snippet-service + nats: + url: "" + pendingLimits: + msgLimit: "524288" + bytesLimit: "67108864" + tls: + caPool: + keyFile: + certFile: + useSystemCAPool: false + storage: + use: mongoDB + mongoDB: + uri: + database: snippetService + maxPoolSize: 16 + maxConnIdleTime: 4m0s + tls: + caPool: + keyFile: + certFile: + useSystemCAPool: false + resourceUpdater: + cleanUpExpiredUpdates: "0 * * * *" + grpc: + address: "" + sendMsgSize: 4194304 + recvMsgSize: 4194304 + keepAlive: + time: 10s + timeout: 20s + permitWithoutStream: true + tls: + caPool: + keyFile: + certFile: + useSystemCAPool: false mockoauthserver: oauth: diff --git a/cloud2cloud-gateway/service/retrieveDevice.go b/cloud2cloud-gateway/service/retrieveDevice.go index 36fde2079..7cf16ee97 100644 --- a/cloud2cloud-gateway/service/retrieveDevice.go +++ b/cloud2cloud-gateway/service/retrieveDevice.go @@ -10,13 +10,10 @@ import ( "github.com/gorilla/mux" "github.com/plgd-dev/device/v2/schema" - "github.com/plgd-dev/go-coap/v3/message" pbGRPC "github.com/plgd-dev/hub/v2/grpc-gateway/pb" "github.com/plgd-dev/hub/v2/pkg/log" kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" - "github.com/plgd-dev/kit/v2/codec/cbor" - "github.com/plgd-dev/kit/v2/codec/json" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -77,34 +74,10 @@ type Representation struct { } func unmarshalContent(c *commands.Content, m interface{}) error { - switch c.GetContentType() { - case message.AppCBOR.String(), message.AppOcfCbor.String(): - err := cbor.Decode(c.GetData(), m) - if err != nil { - return fmt.Errorf("cannot unmarshal resource content: %w", err) - } - case message.AppJSON.String(): - err := json.Decode(c.GetData(), m) - if err != nil { - return fmt.Errorf("cannot unmarshal resource content: %w", err) - } - case message.TextPlain.String(): - switch v := m.(type) { - case *string: - *v = string(c.GetData()) - case *[]byte: - *v = c.GetData() - case *interface{}: - *v = string(c.GetData()) - default: - return fmt.Errorf("cannot unmarshal resource content: invalid type (%T)", m) - } - case "": + if c.GetContentType() == "" { return nil - default: - return fmt.Errorf("cannot unmarshal resource content: unknown content type (%v)", c.GetContentType()) } - return nil + return commands.DecodeContent(c, m) } func (rh *RequestHandler) RetrieveResources(ctx context.Context, resourceIdFilter []*pbGRPC.ResourceIdFilter, deviceIdFilter []string) (map[string][]Representation, error) { diff --git a/cloud2cloud-gateway/test/test.go b/cloud2cloud-gateway/test/test.go index f9924c0e7..500dbee87 100644 --- a/cloud2cloud-gateway/test/test.go +++ b/cloud2cloud-gateway/test/test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "net" + "slices" "sync" "testing" "time" @@ -15,7 +16,6 @@ import ( "github.com/plgd-dev/hub/v2/pkg/mongodb" "github.com/plgd-dev/hub/v2/pkg/net/http" "github.com/plgd-dev/hub/v2/pkg/security/certManager/server" - pkgStrings "github.com/plgd-dev/hub/v2/pkg/strings" "github.com/plgd-dev/hub/v2/test/config" testHttp "github.com/plgd-dev/hub/v2/test/http" "github.com/stretchr/testify/require" @@ -38,7 +38,7 @@ func MakeConfig(t require.TestingT) service.Config { cfg.APIs.HTTP.Connection = config.MakeListenerConfig(config.C2C_GW_HOST) cfg.APIs.HTTP.Connection.TLS.ClientCertificateRequired = false - cfg.APIs.HTTP.Authorization = config.MakeAuthorizationConfig() + cfg.APIs.HTTP.Authorization = config.MakeValidatorConfig() cfg.APIs.HTTP.Server = config.MakeHttpServerConfig() cfg.Clients.Eventbus.NATS = config.MakeSubscriberConfig() @@ -98,7 +98,7 @@ func C2CURI(uri string) string { func GetUniqueSubscriptionID(subIDS ...string) string { id := uuid.NewString() for { - if !pkgStrings.Contains(subIDS, id) { + if !slices.Contains(subIDS, id) { break } id = uuid.NewString() diff --git a/coap-gateway/service/clientObserveHandler.go b/coap-gateway/service/clientObserveHandler.go index 00f8d4813..a2f265505 100644 --- a/coap-gateway/service/clientObserveHandler.go +++ b/coap-gateway/service/clientObserveHandler.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "slices" "sync" "sync/atomic" @@ -17,7 +18,6 @@ import ( "github.com/plgd-dev/hub/v2/coap-gateway/service/message" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" "github.com/plgd-dev/hub/v2/grpc-gateway/subscription" - "github.com/plgd-dev/hub/v2/pkg/strings" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" ) @@ -119,7 +119,7 @@ func (s *resourceSubscription) isDuplicateEvent(ev *events.ResourceChanged) bool func (s *resourceSubscription) eventHandler(e *pb.Event) error { switch { case e.GetResourceUnpublished() != nil: - if !strings.Contains(e.GetResourceUnpublished().GetHrefs(), s.href) { + if !slices.Contains(e.GetResourceUnpublished().GetHrefs(), s.href) { return nil } s.cancelSubscription(coapCodes.ServiceUnavailable) diff --git a/coap-gateway/service/clientSubscriber.go b/coap-gateway/service/clientSubscriber.go index 28d9e8324..cc761724a 100644 --- a/coap-gateway/service/clientSubscriber.go +++ b/coap-gateway/service/clientSubscriber.go @@ -11,6 +11,19 @@ func (c *session) replaceDeviceSubscriber(deviceSubscriber *grpcClient.DeviceSub return s } +func (c *session) getDeviceSubscriber() *grpcClient.DeviceSubscriber { + c.private.mutex.Lock() + defer c.private.mutex.Unlock() + return c.private.deviceSubscriber +} + +func (c *session) triggerDeviceSubscriber() { + deviceSubscriber := c.getDeviceSubscriber() + if deviceSubscriber != nil { + deviceSubscriber.TriggerGetPendingCommands() + } +} + // Replace the deviceSubscriber instance in the client with nil and clean up the previous instance. func (c *session) closeDeviceSubscriber() error { deviceSubscriber := c.replaceDeviceSubscriber(nil) diff --git a/coap-gateway/service/observation/deviceObserver.go b/coap-gateway/service/observation/deviceObserver.go index baaf63218..cd99da812 100644 --- a/coap-gateway/service/observation/deviceObserver.go +++ b/coap-gateway/service/observation/deviceObserver.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net" + "slices" "github.com/plgd-dev/device/v2/schema" "github.com/plgd-dev/device/v2/schema/interfaces" @@ -16,7 +17,6 @@ import ( "github.com/plgd-dev/hub/v2/grpc-gateway/pb" "github.com/plgd-dev/hub/v2/pkg/log" "github.com/plgd-dev/hub/v2/pkg/net/coap" - pkgStrings "github.com/plgd-dev/hub/v2/pkg/strings" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" pbRD "github.com/plgd-dev/hub/v2/resource-directory/pb" "google.golang.org/grpc" @@ -308,7 +308,7 @@ func IsDiscoveryResourceObservable(links schema.ResourceLinks) (bool, error) { return observable, nil } - return pkgStrings.Contains(res.Interfaces, observeInterface), nil + return slices.Contains(res.Interfaces, observeInterface), nil } func detectObservationType(links schema.ResourceLinks) (ObservationType, error) { diff --git a/coap-gateway/service/resourceDirectory.go b/coap-gateway/service/resourceDirectory.go index 61c6a8dcb..6deccec75 100644 --- a/coap-gateway/service/resourceDirectory.go +++ b/coap-gateway/service/resourceDirectory.go @@ -173,6 +173,8 @@ func resourceDirectoryPublishHandler(req *mux.Message, client *session) (*pool.M if errCode, errO := observeResources(req.Context(), client, w, req.Sequence()); errO != nil { return nil, statusErrorf(errCode, "%w", errO) } + // trigger device subscriber to get pending commands for the resources that have been published + client.triggerDeviceSubscriber() accept := coapconv.GetAccept(req.Options()) encode, err := coapconv.GetEncoder(accept) diff --git a/coap-gateway/service/signIn.go b/coap-gateway/service/signIn.go index fcf6ff884..a3b5551c0 100644 --- a/coap-gateway/service/signIn.go +++ b/coap-gateway/service/signIn.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "time" "github.com/plgd-dev/go-coap/v3/message" @@ -14,7 +15,6 @@ import ( grpcgwClient "github.com/plgd-dev/hub/v2/grpc-gateway/client" "github.com/plgd-dev/hub/v2/identity-store/events" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" - "github.com/plgd-dev/hub/v2/pkg/strings" pkgTime "github.com/plgd-dev/hub/v2/pkg/time" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/kit/v2/codec/cbor" @@ -173,7 +173,7 @@ func subscribeToDeviceEvents(client *session, owner, deviceID string) error { if evt.GetOwner() != owner { return } - if !strings.Contains(evt.GetDeviceIds(), deviceID) { + if !slices.Contains(evt.GetDeviceIds(), deviceID) { return } client.Close() diff --git a/coap-gateway/test/test.go b/coap-gateway/test/test.go index fe78b1527..ad4884ff8 100644 --- a/coap-gateway/test/test.go +++ b/coap-gateway/test/test.go @@ -52,7 +52,7 @@ func MakeConfig(t require.TestingT) service.Config { Config: config.MakeDeviceAuthorization(), }, }, - Authority: config.MakeAuthorizationConfig(), + Authority: config.MakeValidatorConfig(), } cfg.Clients.IdentityStore.Connection = config.MakeGrpcClientConfig(config.IDENTITY_STORE_HOST) cfg.Clients.ResourceAggregate.Connection = config.MakeGrpcClientConfig(config.RESOURCE_AGGREGATE_HOST) diff --git a/go.mod b/go.mod index 0f0702299..4393d4499 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 github.com/hashicorp/go-multierror v1.1.1 + github.com/itchyny/gojq v0.12.16 github.com/jessevdk/go-flags v1.6.1 github.com/json-iterator/go v1.1.12 github.com/jtacoma/uritemplates v1.0.0 @@ -56,6 +57,7 @@ require ( golang.org/x/net v0.26.0 golang.org/x/oauth2 v0.21.0 golang.org/x/sync v0.7.0 + google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 google.golang.org/grpc v1.64.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.4.0 @@ -86,6 +88,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/huandu/xstrings v1.0.0 // indirect github.com/imdario/mergo v0.3.4 // indirect + github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/jhump/protoreflect v1.16.0 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/klauspost/compress v1.17.9 // indirect @@ -118,7 +121,6 @@ require ( golang.org/x/crypto v0.24.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect gopkg.in/inf.v0 v0.9.1 // indirect ) diff --git a/go.sum b/go.sum index 6e0c4a23e..0009ba293 100644 --- a/go.sum +++ b/go.sum @@ -143,6 +143,10 @@ github.com/huandu/xstrings v1.0.0 h1:pO2K/gKgKaat5LdpAhxhluX2GPQMaI3W5FUz/I/UnWk github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/imdario/mergo v0.3.4 h1:mKkfHkZWD8dC7WxKx3N9WCF0Y+dLau45704YQmY6H94= github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/itchyny/gojq v0.12.16 h1:yLfgLxhIr/6sJNVmYfQjTIv0jGctu6/DgDoivmxTr7g= +github.com/itchyny/gojq v0.12.16/go.mod h1:6abHbdC2uB9ogMS38XsErnfqJ94UlngIJGlRAIj4jTM= +github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= +github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= diff --git a/grpc-gateway/client/client_test.go b/grpc-gateway/client/client_test.go index 94555f8c9..bfb571a1d 100644 --- a/grpc-gateway/client/client_test.go +++ b/grpc-gateway/client/client_test.go @@ -2,13 +2,9 @@ package client_test import ( "context" - "crypto/tls" - "crypto/x509" - "testing" "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/go-coap/v3/message" - "github.com/plgd-dev/hub/v2/grpc-gateway/client" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" "github.com/plgd-dev/hub/v2/pkg/net/grpc/server" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" @@ -16,7 +12,6 @@ import ( "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" "github.com/plgd-dev/kit/v2/codec/cbor" - "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -26,22 +21,6 @@ const ( TestManufacturer = "Test Manufacturer" ) -func NewTestClient(t *testing.T) *client.Client { - rootCAs := x509.NewCertPool() - for _, c := range test.GetRootCertificateAuthorities(t) { - rootCAs.AddCert(c) - } - tlsCfg := tls.Config{ - RootCAs: rootCAs, - } - clientConfig := client.Config{ - GatewayAddress: config.GRPC_GW_HOST, - } - c, err := client.NewFromConfig(&clientConfig, &tlsCfg) - require.NoError(t, err) - return c -} - func NewGateway(addr string) (*server.Server, error) { s, err := server.NewServer(addr) if err != nil { diff --git a/grpc-gateway/client/createResource_test.go b/grpc-gateway/client/createResource_test.go index aad482a4c..745e73b7e 100644 --- a/grpc-gateway/client/createResource_test.go +++ b/grpc-gateway/client/createResource_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/plgd-dev/device/v2/schema/device" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" @@ -67,7 +68,7 @@ func TestClient_CreateResource(t *testing.T) { ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { err := c.Close() require.NoError(t, err) diff --git a/grpc-gateway/client/deleteResource_test.go b/grpc-gateway/client/deleteResource_test.go index 9dbc20918..3d53a7fbc 100644 --- a/grpc-gateway/client/deleteResource_test.go +++ b/grpc-gateway/client/deleteResource_test.go @@ -10,6 +10,7 @@ import ( "github.com/plgd-dev/device/v2/schema/resources" "github.com/plgd-dev/hub/v2/grpc-gateway/client" extCodes "github.com/plgd-dev/hub/v2/grpc-gateway/pb/codes" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" @@ -83,7 +84,7 @@ func TestClientDeleteResource(t *testing.T) { ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { err := c.Close() require.NoError(t, err) @@ -172,7 +173,7 @@ func TestClientBatchDeleteResource(t *testing.T) { ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { err := c.Close() require.NoError(t, err) diff --git a/grpc-gateway/client/deviceSubscriber.go b/grpc-gateway/client/deviceSubscriber.go index 23e76688a..a59bde07f 100644 --- a/grpc-gateway/client/deviceSubscriber.go +++ b/grpc-gateway/client/deviceSubscriber.go @@ -266,6 +266,14 @@ func (s *DeviceSubscriber) triggerReconnect() { } } +func (s *DeviceSubscriber) TriggerGetPendingCommands() { + select { + case <-s.done: + case s.reconnectChan <- true: + default: + } +} + func (s *DeviceSubscriber) reconnect() { for { var wantToSetPendingCommandsHandler bool diff --git a/grpc-gateway/client/deviceSubscriptions_test.go b/grpc-gateway/client/deviceSubscriptions_test.go index 069fca867..ee672927b 100644 --- a/grpc-gateway/client/deviceSubscriptions_test.go +++ b/grpc-gateway/client/deviceSubscriptions_test.go @@ -9,6 +9,7 @@ import ( "github.com/plgd-dev/device/v2/schema/platform" "github.com/plgd-dev/go-coap/v3/message" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" @@ -82,7 +83,7 @@ func TestObserveDeviceResourcesRetrieve(t *testing.T) { }() rac := raservice.NewResourceAggregateClient(raConn.GRPC()) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { errC := c.Close() require.NoError(t, errC) @@ -177,7 +178,7 @@ func TestObserveDeviceResourcesUpdate(t *testing.T) { }() rac := raservice.NewResourceAggregateClient(raConn.GRPC()) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { errC := c.Close() require.NoError(t, errC) @@ -306,7 +307,7 @@ func TestObserveDeviceResourcesCreateAndDelete(t *testing.T) { }() rac := raservice.NewResourceAggregateClient(raConn.GRPC()) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { errC := c.Close() require.NoError(t, errC) diff --git a/grpc-gateway/client/getDevice_test.go b/grpc-gateway/client/getDevice_test.go index 525d5f517..28399270f 100644 --- a/grpc-gateway/client/getDevice_test.go +++ b/grpc-gateway/client/getDevice_test.go @@ -10,6 +10,7 @@ import ( "github.com/plgd-dev/device/v2/test/resource/types" "github.com/plgd-dev/hub/v2/grpc-gateway/client" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" test "github.com/plgd-dev/hub/v2/test" @@ -92,7 +93,7 @@ func TestClient_GetDevice(t *testing.T) { ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { err := c.Close() require.NoError(t, err) diff --git a/grpc-gateway/client/getDevices_test.go b/grpc-gateway/client/getDevices_test.go index e92774ab1..546bc7eb5 100644 --- a/grpc-gateway/client/getDevices_test.go +++ b/grpc-gateway/client/getDevices_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/plgd-dev/hub/v2/grpc-gateway/client" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" test "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" @@ -69,7 +70,7 @@ func TestClient_GetDevices(t *testing.T) { ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { err := c.Close() require.NoError(t, err) diff --git a/grpc-gateway/client/getResource_test.go b/grpc-gateway/client/getResource_test.go index 6131789f8..2467f73ab 100644 --- a/grpc-gateway/client/getResource_test.go +++ b/grpc-gateway/client/getResource_test.go @@ -8,6 +8,7 @@ import ( "github.com/plgd-dev/device/v2/schema/configuration" "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/hub/v2/grpc-gateway/client" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" @@ -100,7 +101,7 @@ func TestClientGetResource(t *testing.T) { ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { err := c.Close() require.NoError(t, err) diff --git a/grpc-gateway/client/maintenance.go b/grpc-gateway/client/maintenance.go index 088ee18ec..1201aa0cc 100644 --- a/grpc-gateway/client/maintenance.go +++ b/grpc-gateway/client/maintenance.go @@ -3,10 +3,10 @@ package client import ( "context" "net/http" + "slices" "github.com/plgd-dev/device/v2/schema/maintenance" "github.com/plgd-dev/hub/v2/pkg/net/grpc" - "github.com/plgd-dev/hub/v2/pkg/strings" "github.com/plgd-dev/hub/v2/resource-aggregate/events" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -65,7 +65,7 @@ func (c *Client) updateMaintenanceResource( } var href string for _, r := range v.GetResources() { - if r.GetDeviceId() == deviceID && strings.Contains(r.GetResourceTypes(), maintenance.ResourceType) { + if r.GetDeviceId() == deviceID && slices.Contains(r.GetResourceTypes(), maintenance.ResourceType) { href = r.GetHref() break } diff --git a/grpc-gateway/client/maintenance_test.go b/grpc-gateway/client/maintenance_test.go index 7256a3b0e..6ec137392 100644 --- a/grpc-gateway/client/maintenance_test.go +++ b/grpc-gateway/client/maintenance_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" test "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" @@ -50,7 +51,7 @@ func TestClientFactoryReset(t *testing.T) { ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { err := c.Close() require.NoError(t, err) @@ -104,7 +105,7 @@ func TestClientReboot(t *testing.T) { ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { err := c.Close() require.NoError(t, err) diff --git a/grpc-gateway/client/observeDeviceResources_test.go b/grpc-gateway/client/observeDeviceResources_test.go index 9a05ff6a8..0c29b82c7 100644 --- a/grpc-gateway/client/observeDeviceResources_test.go +++ b/grpc-gateway/client/observeDeviceResources_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/resource-aggregate/events" test "github.com/plgd-dev/hub/v2/test" @@ -24,7 +25,7 @@ func TestObserveDeviceResourcesPublish(t *testing.T) { defer tearDown() ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { err := c.Close() require.NoError(t, err) diff --git a/grpc-gateway/client/observeDevices_test.go b/grpc-gateway/client/observeDevices_test.go index 6f32642a2..964bcf8cc 100644 --- a/grpc-gateway/client/observeDevices_test.go +++ b/grpc-gateway/client/observeDevices_test.go @@ -7,6 +7,7 @@ import ( "testing" client "github.com/plgd-dev/hub/v2/grpc-gateway/client" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" @@ -23,7 +24,7 @@ func TestObserveDevices(t *testing.T) { defer tearDown() ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { err := c.Close() require.NoError(t, err) diff --git a/grpc-gateway/client/observeResource_test.go b/grpc-gateway/client/observeResource_test.go index cdeaca197..1e7e81c4b 100644 --- a/grpc-gateway/client/observeResource_test.go +++ b/grpc-gateway/client/observeResource_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/plgd-dev/device/v2/schema/configuration" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" @@ -25,7 +26,7 @@ func TestObservingResource(t *testing.T) { defer tearDown() ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { errC := c.Close() require.NoError(t, errC) diff --git a/grpc-gateway/client/updateResource_test.go b/grpc-gateway/client/updateResource_test.go index f77e1e8f9..e2a69ae1f 100644 --- a/grpc-gateway/client/updateResource_test.go +++ b/grpc-gateway/client/updateResource_test.go @@ -8,6 +8,7 @@ import ( "github.com/plgd-dev/device/v2/schema/configuration" "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/hub/v2/grpc-gateway/client" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" @@ -94,7 +95,7 @@ func TestClientUpdateResource(t *testing.T) { ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { errC := c.Close() require.NoError(t, errC) @@ -131,7 +132,7 @@ func TestUpdateConfigurationName(t *testing.T) { defer tearDown() ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) - c := NewTestClient(t) + c := grpcgwTest.NewTestClient(t) defer func() { err := c.Close() require.NoError(t, err) diff --git a/grpc-gateway/pb/README.md b/grpc-gateway/pb/README.md index e4b6b1c72..086a6ba58 100644 --- a/grpc-gateway/pb/README.md +++ b/grpc-gateway/pb/README.md @@ -223,6 +223,8 @@ | resource_id | [resourceaggregate.pb.ResourceId](#resourceaggregate-pb-ResourceId) | | | | content | [Content](#grpcgateway-pb-Content) | | | | time_to_live | [int64](#int64) | | command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). | +| force | [bool](#bool) | | if true, the command will be executed even if the resource does not exist | +| async | [bool](#bool) | | if true, the command will finish immediately after pending event is created | @@ -285,6 +287,8 @@ | resource_id | [resourceaggregate.pb.ResourceId](#resourceaggregate-pb-ResourceId) | | | | time_to_live | [int64](#int64) | | command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). | | resource_interface | [string](#string) | | | +| force | [bool](#bool) | | if true, the command will be executed even if the resource does not exist | +| async | [bool](#bool) | | if true, the command will finish immediately after pending event is created | @@ -695,6 +699,8 @@ Certain filters perform a logical "or" operation among the elements of t | resource_interface | [string](#string) | | | | time_to_live | [int64](#int64) | | command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). | | content | [Content](#grpcgateway-pb-Content) | | | +| force | [bool](#bool) | | if true, the command will be executed even if the resource does not exist | +| async | [bool](#bool) | | if true, the command will finish immediately after pending event is created | @@ -905,6 +911,7 @@ Certain filters perform a logical "or" operation among the elements of t | device_id_filter | [string](#string) | repeated | | | type_filter | [string](#string) | repeated | | | resource_id_filter | [ResourceIdFilter](#grpcgateway-pb-ResourceIdFilter) | repeated | New resource ID filter. For HTTP requests, use it multiple times as a query parameter like "resourceIdFilter={deviceID}{href}". | +| include_hidden_resources | [bool](#bool) | | Get all pending commands for all resources, even if the resource is not published. | @@ -1050,6 +1057,7 @@ UI configuration | ----- | ---- | ----- | ----------- | | visibility | [UIVisibility](#grpcgateway-pb-UIVisibility) | | | | device_provisioning_service | [string](#string) | | Address to device provisioning service HTTP API in format https://host:port | +| snippet_service | [string](#string) | | Address to snippet service HTTP API in format https://host:port | diff --git a/grpc-gateway/pb/createResource.go b/grpc-gateway/pb/createResource.go index 68476c13c..6de21b418 100644 --- a/grpc-gateway/pb/createResource.go +++ b/grpc-gateway/pb/createResource.go @@ -5,6 +5,7 @@ import ( "github.com/google/uuid" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/resource-aggregate/events" "google.golang.org/grpc/peer" ) @@ -35,5 +36,13 @@ func (req *CreateResourceRequest) ToRACommand(ctx context.Context) (*commands.Cr CommandMetadata: &commands.CommandMetadata{ ConnectionId: connectionID, }, + Force: req.GetForce(), }, nil } + +func (x *CreateResourceResponse) SetData(data *events.ResourceCreated) { + if x == nil { + return + } + x.Data = data +} diff --git a/grpc-gateway/pb/deleteResource.go b/grpc-gateway/pb/deleteResource.go index db1b5e557..a632fda2d 100644 --- a/grpc-gateway/pb/deleteResource.go +++ b/grpc-gateway/pb/deleteResource.go @@ -5,6 +5,7 @@ import ( "github.com/google/uuid" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/resource-aggregate/events" "google.golang.org/grpc/peer" ) @@ -30,5 +31,13 @@ func (req *DeleteResourceRequest) ToRACommand(ctx context.Context) (*commands.De ConnectionId: connectionID, }, ResourceInterface: req.GetResourceInterface(), + Force: req.GetForce(), }, nil } + +func (x *DeleteResourceResponse) SetData(data *events.ResourceDeleted) { + if x == nil { + return + } + x.Data = data +} diff --git a/grpc-gateway/pb/devices.pb.go b/grpc-gateway/pb/devices.pb.go index 49f7d3df8..8c27f6cd0 100644 --- a/grpc-gateway/pb/devices.pb.go +++ b/grpc-gateway/pb/devices.pb.go @@ -781,6 +781,8 @@ type UpdateResourceRequest struct { ResourceInterface string `protobuf:"bytes,3,opt,name=resource_interface,json=resourceInterface,proto3" json:"resource_interface,omitempty"` TimeToLive int64 `protobuf:"varint,4,opt,name=time_to_live,json=timeToLive,proto3" json:"time_to_live,omitempty"` // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). Content *Content `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + Force bool `protobuf:"varint,5,opt,name=force,proto3" json:"force,omitempty"` // if true, the command will be executed even if the resource does not exist + Async bool `protobuf:"varint,6,opt,name=async,proto3" json:"async,omitempty"` // if true, the command will finish immediately after pending event is created } func (x *UpdateResourceRequest) Reset() { @@ -843,6 +845,20 @@ func (x *UpdateResourceRequest) GetContent() *Content { return nil } +func (x *UpdateResourceRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + +func (x *UpdateResourceRequest) GetAsync() bool { + if x != nil { + return x.Async + } + return false +} + type UpdateResourceResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1533,6 +1549,8 @@ type DeleteResourceRequest struct { ResourceId *commands.ResourceId `protobuf:"bytes,1,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` TimeToLive int64 `protobuf:"varint,2,opt,name=time_to_live,json=timeToLive,proto3" json:"time_to_live,omitempty"` // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). ResourceInterface string `protobuf:"bytes,3,opt,name=resource_interface,json=resourceInterface,proto3" json:"resource_interface,omitempty"` + Force bool `protobuf:"varint,4,opt,name=force,proto3" json:"force,omitempty"` // if true, the command will be executed even if the resource does not exist + Async bool `protobuf:"varint,5,opt,name=async,proto3" json:"async,omitempty"` // if true, the command will finish immediately after pending event is created } func (x *DeleteResourceRequest) Reset() { @@ -1588,6 +1606,20 @@ func (x *DeleteResourceRequest) GetResourceInterface() string { return "" } +func (x *DeleteResourceRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + +func (x *DeleteResourceRequest) GetAsync() bool { + if x != nil { + return x.Async + } + return false +} + type DeleteResourceResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1643,6 +1675,8 @@ type CreateResourceRequest struct { ResourceId *commands.ResourceId `protobuf:"bytes,1,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` Content *Content `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` TimeToLive int64 `protobuf:"varint,3,opt,name=time_to_live,json=timeToLive,proto3" json:"time_to_live,omitempty"` // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). + Force bool `protobuf:"varint,4,opt,name=force,proto3" json:"force,omitempty"` // if true, the command will be executed even if the resource does not exist + Async bool `protobuf:"varint,5,opt,name=async,proto3" json:"async,omitempty"` // if true, the command will finish immediately after pending event is created } func (x *CreateResourceRequest) Reset() { @@ -1698,6 +1732,20 @@ func (x *CreateResourceRequest) GetTimeToLive() int64 { return 0 } +func (x *CreateResourceRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + +func (x *CreateResourceRequest) GetAsync() bool { + if x != nil { + return x.Async + } + return false +} + type CreateResourceResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2309,7 +2357,7 @@ var file_grpc_gateway_pb_devices_proto_rawDesc = []byte{ 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, - 0x22, 0xde, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x22, 0x8a, 0x02, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, @@ -2323,341 +2371,349 @@ var file_grpc_gateway_pb_devices_proto_rawDesc = []byte{ 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, - 0x74, 0x22, 0x53, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x04, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, - 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xa6, 0x08, 0x0a, 0x11, 0x53, 0x75, 0x62, 0x73, 0x63, - 0x72, 0x69, 0x62, 0x65, 0x54, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x67, 0x0a, 0x13, - 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x67, 0x72, 0x70, 0x63, - 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, - 0x72, 0x69, 0x62, 0x65, 0x54, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x48, - 0x00, 0x52, 0x12, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x67, 0x0a, 0x13, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x5f, - 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x54, 0x6f, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x75, 0x62, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x12, 0x63, 0x61, 0x6e, 0x63, - 0x65, 0x6c, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x73, 0x79, 0x6e, 0x63, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x61, 0x73, 0x79, 0x6e, 0x63, 0x22, 0x53, 0x0a, + 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x52, 0x04, 0x64, 0x61, + 0x74, 0x61, 0x22, 0xa6, 0x08, 0x0a, 0x11, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, + 0x54, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x67, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, + 0x54, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, + 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x12, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x67, 0x0a, 0x13, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x5f, 0x73, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, + 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x54, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x12, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, + 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x65, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x64, 0x1a, 0xce, 0x05, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5d, 0x0a, 0x0c, 0x65, 0x76, 0x65, 0x6e, + 0x74, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x3a, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, + 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x54, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x76, 0x65, 0x6e, + 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x12, 0x39, 0x0a, 0x17, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x14, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, + 0x68, 0x72, 0x65, 0x66, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0a, 0x68, 0x72, 0x65, 0x66, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x4e, 0x0a, + 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x10, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x82, 0x03, + 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x0a, 0x52, 0x45, 0x47, 0x49, 0x53, + 0x54, 0x45, 0x52, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x52, 0x45, 0x47, + 0x49, 0x53, 0x54, 0x45, 0x52, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x56, + 0x49, 0x43, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x55, 0x50, 0x44, + 0x41, 0x54, 0x45, 0x44, 0x10, 0x04, 0x12, 0x22, 0x0a, 0x1e, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, + 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, + 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x45, + 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, 0x44, + 0x10, 0x06, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x55, + 0x4e, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0x07, 0x12, 0x1b, 0x0a, 0x17, + 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, + 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x08, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x45, 0x53, + 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x09, 0x12, + 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x52, 0x45, 0x54, 0x52, + 0x49, 0x45, 0x56, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x0a, 0x12, 0x16, + 0x0a, 0x12, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x49, + 0x45, 0x56, 0x45, 0x44, 0x10, 0x0b, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, + 0x43, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, + 0x47, 0x10, 0x0c, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, + 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x0d, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x53, + 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, + 0x44, 0x49, 0x4e, 0x47, 0x10, 0x0e, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, + 0x43, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x0f, 0x12, 0x14, 0x0a, 0x10, + 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x44, + 0x10, 0x10, 0x1a, 0x3d, 0x0a, 0x12, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x64, 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x85, 0x15, 0x0a, 0x05, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, - 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x1a, 0xce, 0x05, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5d, 0x0a, 0x0c, - 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0e, 0x32, 0x3a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x54, 0x6f, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x75, 0x62, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x0b, - 0x65, 0x76, 0x65, 0x6e, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x64, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x46, - 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x17, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x14, 0x68, 0x74, 0x74, 0x70, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x72, 0x65, 0x66, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x68, 0x72, 0x65, 0x66, 0x46, 0x69, 0x6c, 0x74, 0x65, - 0x72, 0x12, 0x4e, 0x0a, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, - 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, - 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, - 0x72, 0x22, 0x82, 0x03, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x0a, 0x52, - 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x55, - 0x4e, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1b, 0x0a, - 0x17, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, - 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x04, 0x12, 0x22, 0x0a, 0x1e, 0x44, 0x45, - 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x55, 0x50, - 0x44, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x16, - 0x0a, 0x12, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x50, 0x55, 0x42, 0x4c, 0x49, - 0x53, 0x48, 0x45, 0x44, 0x10, 0x06, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, - 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0x07, - 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x55, 0x50, 0x44, - 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x08, 0x12, 0x14, 0x0a, - 0x10, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, - 0x44, 0x10, 0x09, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, - 0x52, 0x45, 0x54, 0x52, 0x49, 0x45, 0x56, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, - 0x10, 0x0a, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x52, - 0x45, 0x54, 0x52, 0x49, 0x45, 0x56, 0x45, 0x44, 0x10, 0x0b, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, - 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x50, 0x45, - 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x0c, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x45, 0x53, 0x4f, 0x55, - 0x52, 0x43, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x0d, 0x12, 0x1b, 0x0a, - 0x17, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, - 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x0e, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x45, - 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x0f, - 0x12, 0x14, 0x0a, 0x10, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x48, 0x41, - 0x4e, 0x47, 0x45, 0x44, 0x10, 0x10, 0x1a, 0x3d, 0x0a, 0x12, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, - 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0f, - 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x42, 0x08, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, - 0x85, 0x15, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, - 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x55, 0x0a, 0x11, 0x64, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, - 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x48, 0x00, 0x52, 0x10, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, - 0x12, 0x5b, 0x0a, 0x13, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x75, 0x6e, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x55, 0x6e, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x48, 0x00, 0x52, 0x12, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x5d, 0x0a, - 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, - 0x68, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x73, 0x50, 0x75, - 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x64, 0x48, 0x00, 0x52, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x63, 0x0a, 0x14, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x75, 0x6e, 0x70, 0x75, 0x62, 0x6c, 0x69, - 0x73, 0x68, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, - 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x73, 0x55, - 0x6e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x64, 0x48, 0x00, 0x52, 0x13, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x6e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, - 0x64, 0x12, 0x52, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, - 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x64, 0x48, 0x00, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x5b, 0x0a, 0x13, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x48, 0x00, 0x52, 0x12, - 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, - 0x65, 0x64, 0x12, 0x61, 0x0a, 0x15, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x2a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, - 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x48, 0x00, 0x52, - 0x14, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6e, - 0x63, 0x65, 0x6c, 0x65, 0x64, 0x12, 0x65, 0x0a, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, - 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x52, 0x0a, 0x10, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, - 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, - 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, - 0x12, 0x6b, 0x0a, 0x19, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x74, - 0x72, 0x69, 0x65, 0x76, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0e, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, - 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, - 0x6e, 0x67, 0x48, 0x00, 0x52, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, - 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x58, 0x0a, - 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, - 0x76, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, - 0x65, 0x64, 0x48, 0x00, 0x52, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, - 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x64, 0x12, 0x65, 0x0a, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, - 0x6e, 0x67, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x65, - 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x52, - 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x48, - 0x00, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x64, 0x12, 0x65, 0x0a, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x12, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, - 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x52, 0x0a, 0x10, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x13, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, - 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0f, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x78, 0x0a, - 0x1e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, - 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x1b, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x65, 0x0a, 0x17, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x64, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x15, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x1a, 0xba, - 0x02, 0x0a, 0x10, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, - 0x64, 0x73, 0x12, 0x46, 0x0a, 0x0e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x65, 0x76, 0x65, - 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x76, 0x0a, 0x16, 0x6f, 0x70, - 0x65, 0x6e, 0x5f, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x61, 0x72, - 0x72, 0x69, 0x65, 0x72, 0x18, 0x64, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x67, 0x72, 0x70, - 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x65, 0x64, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, - 0x43, 0x61, 0x72, 0x72, 0x69, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x6f, 0x70, - 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x43, 0x61, 0x72, 0x72, 0x69, - 0x65, 0x72, 0x1a, 0x47, 0x0a, 0x19, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, - 0x74, 0x72, 0x79, 0x43, 0x61, 0x72, 0x72, 0x69, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0xbe, 0x02, 0x0a, 0x12, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, - 0x73, 0x12, 0x46, 0x0a, 0x0e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x65, 0x76, 0x65, 0x6e, - 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x78, 0x0a, 0x16, 0x6f, 0x70, 0x65, - 0x6e, 0x5f, 0x74, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x61, 0x72, 0x72, - 0x69, 0x65, 0x72, 0x18, 0x64, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x55, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x26, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, + 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x48, 0x00, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x5b, 0x0a, 0x13, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x75, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x65, 0x64, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, - 0x79, 0x43, 0x61, 0x72, 0x72, 0x69, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x6f, - 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x43, 0x61, 0x72, 0x72, - 0x69, 0x65, 0x72, 0x1a, 0x47, 0x0a, 0x19, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, - 0x65, 0x74, 0x72, 0x79, 0x43, 0x61, 0x72, 0x72, 0x69, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x90, 0x02, 0x0a, - 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, - 0x73, 0x65, 0x64, 0x12, 0x57, 0x0a, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x67, 0x72, 0x70, 0x63, - 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, - 0x73, 0x65, 0x64, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, - 0x0b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, 0xa0, 0x01, 0x0a, - 0x0b, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x4d, 0x0a, 0x04, - 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x39, 0x2e, 0x67, 0x72, 0x70, - 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, - 0x74, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, - 0x73, 0x73, 0x65, 0x64, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x2e, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x28, 0x0a, 0x04, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x06, 0x0a, - 0x02, 0x4f, 0x4b, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, - 0x12, 0x0d, 0x0a, 0x09, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x02, 0x1a, - 0x2e, 0x0a, 0x14, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, - 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x42, - 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x43, 0x0a, 0x0f, 0x4c, 0x6f, 0x63, 0x61, 0x6c, - 0x69, 0x7a, 0x65, 0x64, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, - 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, - 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x93, 0x06, 0x0a, - 0x06, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x12, 0x12, 0x0a, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x4c, - 0x0a, 0x11, 0x6d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x72, 0x70, 0x63, - 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, - 0x69, 0x7a, 0x65, 0x64, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x10, 0x6d, 0x61, 0x6e, 0x75, - 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, - 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, - 0x1e, 0x0a, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x18, 0x07, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x12, - 0x36, 0x0a, 0x17, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x69, 0x6e, 0x64, 0x65, - 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x15, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x70, 0x65, - 0x6e, 0x64, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x72, 0x65, 0x64, 0x48, 0x00, 0x52, 0x12, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x55, 0x6e, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x5d, 0x0a, 0x12, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x64, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x52, 0x04, 0x64, 0x61, - 0x74, 0x61, 0x12, 0x51, 0x0a, 0x10, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x5f, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x67, - 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x2e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x0f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, - 0x74, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x73, 0x1a, 0xd3, 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x12, 0x40, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, - 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x5c, 0x0a, 0x14, 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x68, - 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x29, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, - 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x77, 0x69, 0x6e, 0x53, 0x79, 0x6e, 0x63, - 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x74, 0x77, 0x69, - 0x6e, 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x74, 0x77, 0x69, 0x6e, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x47, 0x0a, 0x0f, 0x4f, 0x77, 0x6e, - 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, - 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4f, - 0x57, 0x4e, 0x45, 0x44, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x44, 0x10, - 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x55, 0x50, 0x50, 0x4f, 0x52, 0x54, 0x45, 0x44, - 0x10, 0x03, 0x22, 0x40, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, - 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, - 0x64, 0x61, 0x74, 0x61, 0x22, 0xab, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, - 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, - 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, - 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, - 0x69, 0x76, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, - 0x63, 0x65, 0x22, 0x53, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x04, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, - 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x64, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xaf, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, + 0x68, 0x65, 0x64, 0x48, 0x00, 0x52, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, + 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x63, 0x0a, 0x14, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x75, 0x6e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x64, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x07, - 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, - 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, - 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x22, 0x53, 0x0a, 0x16, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x73, 0x55, 0x6e, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x73, 0x68, 0x65, 0x64, 0x48, 0x00, 0x52, 0x13, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x55, 0x6e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x64, 0x12, 0x52, 0x0a, + 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x48, 0x00, + 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x64, 0x12, 0x5b, 0x0a, 0x13, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, + 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, + 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x48, 0x00, 0x52, 0x12, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x12, 0x61, + 0x0a, 0x15, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, + 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x48, 0x00, 0x52, 0x14, 0x73, 0x75, 0x62, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, + 0x64, 0x12, 0x65, 0x0a, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, + 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, + 0x00, 0x52, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x52, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, + 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0f, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x12, 0x6b, 0x0a, 0x19, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, + 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, + 0x52, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, + 0x76, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x58, 0x0a, 0x12, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x64, 0x18, + 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x64, 0x48, 0x00, + 0x52, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, + 0x76, 0x65, 0x64, 0x12, 0x65, 0x0a, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x10, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, + 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, + 0x67, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x52, 0x0a, 0x10, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x11, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, + 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0f, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x65, + 0x0a, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x15, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x52, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x48, 0x00, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x78, 0x0a, 0x1e, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x31, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, + 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x1b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x12, 0x65, 0x0a, 0x17, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x15, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, + 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x48, 0x00, 0x52, 0x15, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x1a, 0xba, 0x02, 0x0a, 0x10, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, + 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x73, 0x12, 0x46, + 0x0a, 0x0e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x76, 0x0a, 0x16, 0x6f, 0x70, 0x65, 0x6e, 0x5f, 0x74, + 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x61, 0x72, 0x72, 0x69, 0x65, 0x72, + 0x18, 0x64, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x2e, 0x4f, + 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x43, 0x61, 0x72, 0x72, + 0x69, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x6f, 0x70, 0x65, 0x6e, 0x54, 0x65, + 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x43, 0x61, 0x72, 0x72, 0x69, 0x65, 0x72, 0x1a, 0x47, + 0x0a, 0x19, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x43, + 0x61, 0x72, 0x72, 0x69, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0xbe, 0x02, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x1d, + 0x0a, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x73, 0x12, 0x46, 0x0a, + 0x0e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x78, 0x0a, 0x16, 0x6f, 0x70, 0x65, 0x6e, 0x5f, 0x74, 0x65, + 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x61, 0x72, 0x72, 0x69, 0x65, 0x72, 0x18, + 0x64, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x55, 0x6e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x2e, + 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x43, 0x61, 0x72, + 0x72, 0x69, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x6f, 0x70, 0x65, 0x6e, 0x54, + 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x43, 0x61, 0x72, 0x72, 0x69, 0x65, 0x72, 0x1a, + 0x47, 0x0a, 0x19, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, + 0x43, 0x61, 0x72, 0x72, 0x69, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x90, 0x02, 0x0a, 0x12, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x12, + 0x57, 0x0a, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x2e, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0b, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, 0xa0, 0x01, 0x0a, 0x0b, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x4d, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x39, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, + 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x43, 0x6f, 0x64, + 0x65, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x22, 0x28, 0x0a, 0x04, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, + 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, + 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x02, 0x1a, 0x2e, 0x0a, 0x14, 0x53, + 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x42, 0x06, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x22, 0x43, 0x0a, 0x0f, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, + 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, + 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x93, 0x06, 0x0a, 0x06, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3b, 0x0a, + 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, + 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x4c, 0x0a, 0x11, 0x6d, 0x61, + 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x10, 0x6d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, + 0x75, 0x72, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x6f, 0x64, 0x65, + 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x17, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, + 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, + 0x74, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x42, 0x2f, - 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, - 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x67, 0x72, 0x70, - 0x63, 0x2d, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x51, + 0x0a, 0x10, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x5f, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x2e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x0f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x0b, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, + 0xd3, 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x40, 0x0a, 0x0a, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, + 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5c, + 0x0a, 0x14, 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, + 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x77, 0x69, 0x6e, 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x74, 0x77, 0x69, 0x6e, 0x53, 0x79, 0x6e, + 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, + 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0b, 0x74, 0x77, 0x69, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x4a, + 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x47, 0x0a, 0x0f, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, + 0x69, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4f, 0x57, 0x4e, 0x45, 0x44, + 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0f, 0x0a, + 0x0b, 0x55, 0x4e, 0x53, 0x55, 0x50, 0x50, 0x4f, 0x52, 0x54, 0x45, 0x44, 0x10, 0x03, 0x22, 0x40, + 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x22, 0xd7, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x20, 0x0a, + 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x12, + 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, + 0x6f, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x73, 0x79, 0x6e, 0x63, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x05, 0x61, 0x73, 0x79, 0x6e, 0x63, 0x22, 0x53, 0x0a, 0x16, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, + 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, + 0xdb, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, + 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, + 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x07, + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, + 0x20, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x73, 0x79, 0x6e, 0x63, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x61, 0x73, 0x79, 0x6e, 0x63, 0x22, 0x53, 0x0a, + 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x52, 0x04, 0x64, 0x61, + 0x74, 0x61, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, + 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x70, 0x62, + 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/grpc-gateway/pb/devices.proto b/grpc-gateway/pb/devices.proto index 54acada11..19fbe585e 100644 --- a/grpc-gateway/pb/devices.proto +++ b/grpc-gateway/pb/devices.proto @@ -67,6 +67,8 @@ message UpdateResourceRequest { string resource_interface = 3; int64 time_to_live = 4; // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). Content content = 2; + bool force = 5; // if true, the command will be executed even if the resource does not exist + bool async = 6; // if true, the command will finish immediately after pending event is created } message UpdateResourceResponse { @@ -220,6 +222,8 @@ message DeleteResourceRequest { resourceaggregate.pb.ResourceId resource_id = 1; int64 time_to_live = 2; // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). string resource_interface = 3; + bool force = 4; // if true, the command will be executed even if the resource does not exist + bool async = 5; // if true, the command will finish immediately after pending event is created } message DeleteResourceResponse { @@ -230,6 +234,8 @@ message CreateResourceRequest { resourceaggregate.pb.ResourceId resource_id = 1; Content content = 2; int64 time_to_live = 3; // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). + bool force = 4; // if true, the command will be executed even if the resource does not exist + bool async = 5; // if true, the command will finish immediately after pending event is created } message CreateResourceResponse { diff --git a/grpc-gateway/pb/doc.html b/grpc-gateway/pb/doc.html index 5c2af8ccb..05126e27f 100644 --- a/grpc-gateway/pb/doc.html +++ b/grpc-gateway/pb/doc.html @@ -841,6 +841,20 @@

CreateResourceRequest

command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms).

+ + force + bool + +

if true, the command will be executed even if the resource does not exist

+ + + + async + bool + +

if true, the command will finish immediately after pending event is created

+ + @@ -951,6 +965,20 @@

DeleteResourceRequest

+ + force + bool + +

if true, the command will be executed even if the resource does not exist

+ + + + async + bool + +

if true, the command will finish immediately after pending event is created

+ + @@ -1951,6 +1979,20 @@

UpdateResourceRequest

+ + force + bool + +

if true, the command will be executed even if the resource does not exist

+ + + + async + bool + +

if true, the command will finish immediately after pending event is created

+ + @@ -2483,6 +2525,13 @@

GetPendingCommandsRequest

New resource ID filter. For HTTP requests, use it multiple times as a query parameter like "resourceIdFilter={deviceID}{href}".

+ + include_hidden_resources + bool + +

Get all pending commands for all resources, even if the resource is not published.

+ + @@ -2889,6 +2938,13 @@

UIConfiguration

Address to device provisioning service HTTP API in format https://host:port

+ + snippet_service + string + +

Address to snippet service HTTP API in format https://host:port

+ + diff --git a/grpc-gateway/pb/getPendingCommands.pb.go b/grpc-gateway/pb/getPendingCommands.pb.go index 65363c8a9..ba2113eac 100644 --- a/grpc-gateway/pb/getPendingCommands.pb.go +++ b/grpc-gateway/pb/getPendingCommands.pb.go @@ -83,10 +83,11 @@ type GetPendingCommandsRequest struct { CommandFilter []GetPendingCommandsRequest_Command `protobuf:"varint,1,rep,packed,name=command_filter,json=commandFilter,proto3,enum=grpcgateway.pb.GetPendingCommandsRequest_Command" json:"command_filter,omitempty"` // Deprecated: Marked as deprecated in grpc-gateway/pb/getPendingCommands.proto. - HttpResourceIdFilter []string `protobuf:"bytes,2,rep,name=http_resource_id_filter,json=httpResourceIdFilter,proto3" json:"http_resource_id_filter,omitempty"` - DeviceIdFilter []string `protobuf:"bytes,3,rep,name=device_id_filter,json=deviceIdFilter,proto3" json:"device_id_filter,omitempty"` - TypeFilter []string `protobuf:"bytes,4,rep,name=type_filter,json=typeFilter,proto3" json:"type_filter,omitempty"` - ResourceIdFilter []*ResourceIdFilter `protobuf:"bytes,5,rep,name=resource_id_filter,json=resourceIdFilter,proto3" json:"resource_id_filter,omitempty"` // New resource ID filter. For HTTP requests, use it multiple times as a query parameter like "resourceIdFilter={deviceID}{href}". + HttpResourceIdFilter []string `protobuf:"bytes,2,rep,name=http_resource_id_filter,json=httpResourceIdFilter,proto3" json:"http_resource_id_filter,omitempty"` + DeviceIdFilter []string `protobuf:"bytes,3,rep,name=device_id_filter,json=deviceIdFilter,proto3" json:"device_id_filter,omitempty"` + TypeFilter []string `protobuf:"bytes,4,rep,name=type_filter,json=typeFilter,proto3" json:"type_filter,omitempty"` + ResourceIdFilter []*ResourceIdFilter `protobuf:"bytes,5,rep,name=resource_id_filter,json=resourceIdFilter,proto3" json:"resource_id_filter,omitempty"` // New resource ID filter. For HTTP requests, use it multiple times as a query parameter like "resourceIdFilter={deviceID}{href}". + IncludeHiddenResources bool `protobuf:"varint,6,opt,name=include_hidden_resources,json=includeHiddenResources,proto3" json:"include_hidden_resources,omitempty"` // Get all pending commands for all resources, even if the resource is not published. } func (x *GetPendingCommandsRequest) Reset() { @@ -157,6 +158,13 @@ func (x *GetPendingCommandsRequest) GetResourceIdFilter() []*ResourceIdFilter { return nil } +func (x *GetPendingCommandsRequest) GetIncludeHiddenResources() bool { + if x != nil { + return x.IncludeHiddenResources + } + return false +} + type PendingCommand struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -290,7 +298,7 @@ var file_grpc_gateway_pb_getPendingCommands_proto_rawDesc = []byte{ 0x75, 0x72, 0x63, 0x65, 0x2d, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2f, 0x70, 0x62, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1d, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x70, 0x62, 0x2f, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc8, 0x03, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x82, 0x04, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x58, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, @@ -311,54 +319,58 @@ var file_grpc_gateway_pb_getPendingCommands_proto_rawDesc = []byte{ 0x72, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x7b, 0x0a, 0x07, 0x43, - 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, - 0x43, 0x45, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x52, - 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x49, 0x45, 0x56, 0x45, - 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x55, - 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x45, 0x53, 0x4f, 0x55, - 0x52, 0x43, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x03, 0x12, 0x1a, 0x0a, 0x16, - 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, 0x5f, - 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x04, 0x22, 0xb7, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x6e, - 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x65, 0x0a, 0x17, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x70, - 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, - 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, - 0x6e, 0x67, 0x12, 0x6b, 0x0a, 0x19, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, - 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x50, 0x65, 0x6e, - 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, - 0x65, 0x0a, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, - 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, - 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, - 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x65, 0x0a, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, - 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x65, 0x6e, - 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x78, 0x0a, - 0x1e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x1b, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, - 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x70, 0x62, - 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x18, 0x69, + 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x68, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x5f, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x69, + 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x48, 0x69, 0x64, 0x64, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0x7b, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x52, 0x45, + 0x41, 0x54, 0x45, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, + 0x45, 0x5f, 0x52, 0x45, 0x54, 0x52, 0x49, 0x45, 0x56, 0x45, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, + 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, + 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x44, 0x45, + 0x4c, 0x45, 0x54, 0x45, 0x10, 0x03, 0x12, 0x1a, 0x0a, 0x16, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, + 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, + 0x10, 0x04, 0x22, 0xb7, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x65, 0x0a, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x6b, 0x0a, 0x19, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, + 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, + 0x52, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, + 0x76, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x65, 0x0a, 0x17, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, + 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x12, 0x65, 0x0a, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x64, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, + 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, + 0x52, 0x15, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x78, 0x0a, 0x1e, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x31, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, + 0x6e, 0x67, 0x48, 0x00, 0x52, 0x1b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, + 0x67, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x2f, 0x5a, 0x2d, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, + 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2d, + 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/grpc-gateway/pb/getPendingCommands.proto b/grpc-gateway/pb/getPendingCommands.proto index a2fb881ee..f0f57c9b9 100644 --- a/grpc-gateway/pb/getPendingCommands.proto +++ b/grpc-gateway/pb/getPendingCommands.proto @@ -20,6 +20,7 @@ message GetPendingCommandsRequest { repeated string device_id_filter = 3; repeated string type_filter = 4; repeated ResourceIdFilter resource_id_filter = 5; // New resource ID filter. For HTTP requests, use it multiple times as a query parameter like "resourceIdFilter={deviceID}{href}". + bool include_hidden_resources = 6; // Get all pending commands for all resources, even if the resource is not published. } message PendingCommand { diff --git a/grpc-gateway/pb/hubConfiguration.pb.go b/grpc-gateway/pb/hubConfiguration.pb.go index 67304d6e9..229b6be6f 100644 --- a/grpc-gateway/pb/hubConfiguration.pb.go +++ b/grpc-gateway/pb/hubConfiguration.pb.go @@ -66,7 +66,7 @@ type OAuthClient struct { ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty" yaml:"clientID"` // @gotags: yaml:"clientID" Audience string `protobuf:"bytes,2,opt,name=audience,proto3" json:"audience,omitempty"` Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` - ProviderName string `protobuf:"bytes,4,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty" yaml:"providerName"` // @gotags: yaml:"providerName" + ProviderName string `protobuf:"bytes,4,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty" yaml:"providerName"` // @gotags: yaml:"providerName" ClientAssertionType string `protobuf:"bytes,5,opt,name=client_assertion_type,json=clientAssertionType,proto3" json:"client_assertion_type,omitempty" yaml:"clientAssertionType"` // @gotags: yaml:"clientAssertionType" Authority string `protobuf:"bytes,6,opt,name=authority,proto3" json:"authority,omitempty"` GrantType string `protobuf:"bytes,7,opt,name=grant_type,json=grantType,proto3" json:"grant_type,omitempty" yaml:"grantType"` // @gotags: yaml:"grantType" @@ -296,6 +296,8 @@ type UIConfiguration struct { Visibility *UIVisibility `protobuf:"bytes,1,opt,name=visibility,proto3" json:"visibility,omitempty"` // Address to device provisioning service HTTP API in format https://host:port DeviceProvisioningService string `protobuf:"bytes,2,opt,name=device_provisioning_service,json=deviceProvisioningService,proto3" json:"device_provisioning_service,omitempty"` + // Address to snippet service HTTP API in format https://host:port + SnippetService string `protobuf:"bytes,3,opt,name=snippet_service,json=snippetService,proto3" json:"snippet_service,omitempty"` } func (x *UIConfiguration) Reset() { @@ -344,6 +346,13 @@ func (x *UIConfiguration) GetDeviceProvisioningService() string { return "" } +func (x *UIConfiguration) GetSnippetService() string { + if x != nil { + return x.SnippetService + } + return "" +} + type HubConfigurationResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -731,7 +740,7 @@ var file_grpc_gateway_pb_hubConfiguration_proto_rawDesc = []byte{ 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x70, 0x69, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x68, 0x75, 0x62, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x48, 0x75, - 0x62, 0x22, 0x8f, 0x01, 0x0a, 0x0f, 0x55, 0x49, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x62, 0x22, 0xb8, 0x01, 0x0a, 0x0f, 0x55, 0x49, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, 0x76, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x49, 0x56, 0x69, 0x73, @@ -740,59 +749,62 @@ var file_grpc_gateway_pb_hubConfiguration_proto_rawDesc = []byte{ 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x22, 0x8b, 0x06, 0x0a, 0x18, 0x48, 0x75, 0x62, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x54, - 0x69, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x6a, 0x77, 0x74, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, - 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6a, 0x77, - 0x74, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x2d, 0x0a, 0x13, 0x6a, - 0x77, 0x74, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x63, 0x6c, 0x61, - 0x69, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6a, 0x77, 0x74, 0x44, 0x65, 0x76, - 0x69, 0x63, 0x65, 0x49, 0x64, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, - 0x61, 0x70, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x63, 0x6f, 0x61, 0x70, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x37, 0x0a, - 0x17, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, - 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x74, 0x79, 0x12, 0x3e, 0x0a, 0x1c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, - 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, - 0x6c, 0x69, 0x76, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x18, 0x64, 0x65, 0x66, 0x61, - 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x54, 0x6f, - 0x4c, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x14, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x30, 0x0a, 0x14, 0x68, 0x74, 0x74, - 0x70, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, - 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x68, 0x74, 0x74, 0x70, 0x47, 0x61, 0x74, - 0x65, 0x77, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x45, 0x0a, 0x10, 0x77, - 0x65, 0x62, 0x5f, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, - 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x52, 0x0e, 0x77, 0x65, 0x62, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x43, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x13, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6f, 0x61, 0x75, - 0x74, 0x68, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1b, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, - 0x2e, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x11, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, - 0x45, 0x0a, 0x10, 0x6d, 0x32, 0x6d, 0x5f, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x72, 0x70, 0x63, - 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x4f, 0x41, 0x75, 0x74, 0x68, - 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x0e, 0x6d, 0x32, 0x6d, 0x4f, 0x61, 0x75, 0x74, 0x68, - 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x2f, 0x0a, 0x02, 0x75, 0x69, 0x18, 0x0e, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x49, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x75, 0x69, 0x12, 0x38, 0x0a, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, - 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x72, - 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x42, 0x75, 0x69, - 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, - 0x6f, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, - 0x67, 0x72, 0x70, 0x63, 0x2d, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x70, 0x62, 0x3b, - 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x5f, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x6e, + 0x69, 0x70, 0x70, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x8b, 0x06, 0x0a, + 0x18, 0x48, 0x75, 0x62, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, + 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0f, + 0x6a, 0x77, 0x74, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6a, 0x77, 0x74, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, + 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x2d, 0x0a, 0x13, 0x6a, 0x77, 0x74, 0x5f, 0x64, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x10, 0x6a, 0x77, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x43, 0x6c, + 0x61, 0x69, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x61, 0x70, 0x5f, 0x67, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x61, 0x70, 0x47, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x37, 0x0a, 0x17, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, + 0x1c, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x3e, 0x0a, + 0x1c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x18, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, + 0x15, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x63, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x74, 0x79, 0x12, 0x30, 0x0a, 0x14, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x12, 0x68, 0x74, 0x74, 0x70, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x12, 0x45, 0x0a, 0x10, 0x77, 0x65, 0x62, 0x5f, 0x6f, 0x61, 0x75, 0x74, + 0x68, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, + 0x4f, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x0e, 0x77, 0x65, 0x62, + 0x4f, 0x61, 0x75, 0x74, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x13, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4f, 0x61, 0x75, + 0x74, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x45, 0x0a, 0x10, 0x6d, 0x32, 0x6d, 0x5f, + 0x6f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x2e, 0x70, 0x62, 0x2e, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, + 0x0e, 0x6d, 0x32, 0x6d, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, + 0x2f, 0x0a, 0x02, 0x75, 0x69, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x49, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x75, 0x69, + 0x12, 0x38, 0x0a, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, 0x65, + 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x67, 0x61, + 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/grpc-gateway/pb/hubConfiguration.proto b/grpc-gateway/pb/hubConfiguration.proto index 58503a18d..17d64e90f 100644 --- a/grpc-gateway/pb/hubConfiguration.proto +++ b/grpc-gateway/pb/hubConfiguration.proto @@ -58,6 +58,8 @@ message UIConfiguration { UIVisibility visibility = 1; // Address to device provisioning service HTTP API in format https://host:port string device_provisioning_service = 2; + // Address to snippet service HTTP API in format https://host:port + string snippet_service = 3; } message HubConfigurationResponse { diff --git a/grpc-gateway/pb/service.swagger.json b/grpc-gateway/pb/service.swagger.json index be83686ce..78645c9a0 100644 --- a/grpc-gateway/pb/service.swagger.json +++ b/grpc-gateway/pb/service.swagger.json @@ -356,6 +356,20 @@ "in": "query", "required": false, "type": "string" + }, + { + "name": "force", + "description": "if true, the command will be executed even if the resource does not exist", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "async", + "description": "if true, the command will finish immediately after pending event is created", + "in": "query", + "required": false, + "type": "boolean" } ], "tags": [ @@ -408,6 +422,20 @@ "required": false, "type": "string", "format": "int64" + }, + { + "name": "force", + "description": "if true, the command will be executed even if the resource does not exist", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "async", + "description": "if true, the command will finish immediately after pending event is created", + "in": "query", + "required": false, + "type": "boolean" } ], "tags": [ @@ -530,6 +558,20 @@ "required": false, "type": "string", "format": "int64" + }, + { + "name": "force", + "description": "if true, the command will be executed even if the resource does not exist", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "async", + "description": "if true, the command will finish immediately after pending event is created", + "in": "query", + "required": false, + "type": "boolean" } ], "tags": [ @@ -674,6 +716,13 @@ "type": "string" }, "collectionFormat": "multi" + }, + { + "name": "includeHiddenResources", + "description": "Get all pending commands for all resources, even if the resource is not published.", + "in": "query", + "required": false, + "type": "boolean" } ], "tags": [ @@ -2301,6 +2350,10 @@ "deviceProvisioningService": { "type": "string", "title": "Address to device provisioning service HTTP API in format https://host:port" + }, + "snippetService": { + "type": "string", + "title": "Address to snippet service HTTP API in format https://host:port" } }, "title": "UI configuration" diff --git a/grpc-gateway/pb/updateResource.go b/grpc-gateway/pb/updateResource.go index 9790a5ad7..d4128b0f7 100644 --- a/grpc-gateway/pb/updateResource.go +++ b/grpc-gateway/pb/updateResource.go @@ -5,6 +5,7 @@ import ( "github.com/google/uuid" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/resource-aggregate/events" "google.golang.org/grpc/peer" ) @@ -36,5 +37,13 @@ func (req *UpdateResourceRequest) ToRACommand(ctx context.Context) (*commands.Up CommandMetadata: &commands.CommandMetadata{ ConnectionId: connectionID, }, + Force: req.GetForce(), }, nil } + +func (x *UpdateResourceResponse) SetData(data *events.ResourceUpdated) { + if x == nil { + return + } + x.Data = data +} diff --git a/grpc-gateway/service/createResource.go b/grpc-gateway/service/createResource.go index a633d301d..863b97ab6 100644 --- a/grpc-gateway/service/createResource.go +++ b/grpc-gateway/service/createResource.go @@ -6,25 +6,76 @@ import ( "github.com/plgd-dev/hub/v2/grpc-gateway/pb" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "google.golang.org/grpc" "google.golang.org/grpc/codes" ) -func createResourceError(err error) error { - return kitNetGrpc.ForwardErrorf(codes.Internal, "cannot create resource: %v", err) +func resourceError(action string, err error) error { + return kitNetGrpc.ForwardErrorf(codes.Internal, "cannot %s resource: %v", action, err) } -func (r *RequestHandler) CreateResource(ctx context.Context, req *pb.CreateResourceRequest) (*pb.CreateResourceResponse, error) { - createCommand, err := req.ToRACommand(ctx) +type reqRACommand interface { + *commands.CreateResourceRequest | *commands.DeleteResourceRequest | *commands.UpdateResourceRequest +} + +type respCommand[Event any] interface { + SetData(v Event) +} + +type reqCommand[v reqRACommand] interface { + ToRACommand(ctx context.Context) (v, error) + GetAsync() bool +} + +type ( + syncFunc[Req reqRACommand, Res commands.EventContent] func(ctx context.Context, owner string, req Req) (Res, error) + asyncFunc[Req reqRACommand, Res any] func(ctx context.Context, req Req, opts ...grpc.CallOption) (Res, error) +) + +func handleResourceRequest[ReqRA reqRACommand, Event commands.EventContent, AsyncRes any]( + ctx context.Context, + req reqCommand[ReqRA], + action string, + syncFunc syncFunc[ReqRA, Event], + asyncFunc asyncFunc[ReqRA, AsyncRes], + resp respCommand[Event], +) error { + var err error + var event Event + + raCommand, err := req.ToRACommand(ctx) if err != nil { - return nil, createResourceError(err) + return resourceError(action, err) + } + + if req.GetAsync() { + _, err = asyncFunc(ctx, raCommand) + if err != nil { + return resourceError(action, err) + } + return nil } - createdEvent, err := r.resourceAggregateClient.SyncCreateResource(ctx, "*", createCommand) + event, err = syncFunc(ctx, "*", raCommand) if err != nil { - return nil, createResourceError(err) + return resourceError(action, err) } - if err = commands.CheckEventContent(createdEvent); err != nil { - return nil, createResourceError(err) + if err = commands.CheckEventContent(event); err != nil { + return resourceError(action, err) } - return &pb.CreateResourceResponse{Data: createdEvent}, nil + + resp.SetData(event) + return nil +} + +func (r *RequestHandler) CreateResource(ctx context.Context, req *pb.CreateResourceRequest) (*pb.CreateResourceResponse, error) { + var resp pb.CreateResourceResponse + return &resp, handleResourceRequest( + ctx, + req, + "create", + r.resourceAggregateClient.SyncCreateResource, + r.resourceAggregateClient.CreateResource, + &resp, + ) } diff --git a/grpc-gateway/service/deleteResource.go b/grpc-gateway/service/deleteResource.go index 24a8beb00..3d9ef759b 100644 --- a/grpc-gateway/service/deleteResource.go +++ b/grpc-gateway/service/deleteResource.go @@ -4,26 +4,16 @@ import ( "context" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" - kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" - "github.com/plgd-dev/hub/v2/resource-aggregate/commands" - "google.golang.org/grpc/codes" ) -func deleteResourceError(err error) error { - return kitNetGrpc.ForwardErrorf(codes.Internal, "cannot delete resource: %v", err) -} - func (r *RequestHandler) DeleteResource(ctx context.Context, req *pb.DeleteResourceRequest) (*pb.DeleteResourceResponse, error) { - deleteCommand, err := req.ToRACommand(ctx) - if err != nil { - return nil, deleteResourceError(err) - } - deletedEvent, err := r.resourceAggregateClient.SyncDeleteResource(ctx, "*", deleteCommand) - if err != nil { - return nil, deleteResourceError(err) - } - if err = commands.CheckEventContent(deletedEvent); err != nil { - return nil, deleteResourceError(err) - } - return &pb.DeleteResourceResponse{Data: deletedEvent}, err + var resp pb.DeleteResourceResponse + return &resp, handleResourceRequest( + ctx, + req, + "delete", + r.resourceAggregateClient.SyncDeleteResource, + r.resourceAggregateClient.DeleteResource, + &resp, + ) } diff --git a/grpc-gateway/service/subscribeToEvents_test.go b/grpc-gateway/service/subscribeToEvents_test.go index 913024c9b..b6593e815 100644 --- a/grpc-gateway/service/subscribeToEvents_test.go +++ b/grpc-gateway/service/subscribeToEvents_test.go @@ -667,9 +667,7 @@ func TestRequestHandlerSubscribeForPendingCommands(t *testing.T) { secureGWShutdown() createFn := func(timeToLive time.Duration) { - createCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errC := c.CreateResource(createCtx, &pb.CreateResourceRequest{ + _, errC := c.CreateResource(ctx, &pb.CreateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, device.ResourceURI), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -678,8 +676,9 @@ func TestRequestHandlerSubscribeForPendingCommands(t *testing.T) { }), }, TimeToLive: int64(timeToLive), + Async: true, }) - require.Error(t, errC) + require.NoError(t, errC) } createFn(time.Millisecond * 500) // for test expired event createFn(0) @@ -697,9 +696,7 @@ func TestRequestHandlerSubscribeForPendingCommands(t *testing.T) { retrieveFn(0) updateFn := func(timeToLive time.Duration) { - updateCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errU := c.UpdateResource(updateCtx, &pb.UpdateResourceRequest{ + _, errU := c.UpdateResource(ctx, &pb.UpdateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1")), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -708,20 +705,20 @@ func TestRequestHandlerSubscribeForPendingCommands(t *testing.T) { }), }, TimeToLive: int64(timeToLive), + Async: true, }) - require.Error(t, errU) + require.NoError(t, errU) } updateFn(time.Millisecond * 500) // for test expired event updateFn(0) deleteFn := func(timeToLive time.Duration) { - deleteCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errD := c.DeleteResource(deleteCtx, &pb.DeleteResourceRequest{ + _, errD := c.DeleteResource(ctx, &pb.DeleteResourceRequest{ ResourceId: commands.NewResourceID(deviceID, device.ResourceURI), TimeToLive: int64(timeToLive), + Async: true, }) - require.Error(t, errD) + require.NoError(t, errD) } deleteFn(time.Millisecond * 500) // for test expired event deleteFn(0) diff --git a/grpc-gateway/service/updateResource.go b/grpc-gateway/service/updateResource.go index 715e97dd7..8c8164efb 100644 --- a/grpc-gateway/service/updateResource.go +++ b/grpc-gateway/service/updateResource.go @@ -4,27 +4,16 @@ import ( "context" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" - kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" - "github.com/plgd-dev/hub/v2/resource-aggregate/commands" - "google.golang.org/grpc/codes" ) -func updateResourceError(err error) error { - return kitNetGrpc.ForwardErrorf(codes.Internal, "cannot update resource: %v", err) -} - func (r *RequestHandler) UpdateResource(ctx context.Context, req *pb.UpdateResourceRequest) (*pb.UpdateResourceResponse, error) { - updateCommand, err := req.ToRACommand(ctx) - if err != nil { - return nil, updateResourceError(err) - } - updatedEvent, err := r.resourceAggregateClient.SyncUpdateResource(ctx, "*", updateCommand) - if err != nil { - return nil, updateResourceError(err) - } - err = commands.CheckEventContent(updatedEvent) - if err != nil { - return nil, updateResourceError(err) - } - return &pb.UpdateResourceResponse{Data: updatedEvent}, nil + var resp pb.UpdateResourceResponse + return &resp, handleResourceRequest( + ctx, + req, + "update", + r.resourceAggregateClient.SyncUpdateResource, + r.resourceAggregateClient.UpdateResource, + &resp, + ) } diff --git a/grpc-gateway/service/updateResource_test.go b/grpc-gateway/service/updateResource_test.go index 10e1f05f5..0653992b1 100644 --- a/grpc-gateway/service/updateResource_test.go +++ b/grpc-gateway/service/updateResource_test.go @@ -15,6 +15,8 @@ import ( "github.com/plgd-dev/go-coap/v3/message" coapTest "github.com/plgd-dev/hub/v2/coap-gateway/test" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + isPb "github.com/plgd-dev/hub/v2/identity-store/pb" + isTest "github.com/plgd-dev/hub/v2/identity-store/test" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" @@ -412,3 +414,239 @@ func TestRequestHandlerRunMultipleParallelUpdateResource(t *testing.T) { } wg.Wait() } + +func TestUpdateCreateOnNotExistingResource(t *testing.T) { + deviceID := test.MustFindDeviceByName(test.TestDeviceName) + switchID := "1" + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + isCfg := isTest.MakeConfig(t) + isCfg.APIs.GRPC.TLS.ClientCertificateRequired = false + tearDown := service.SetUp(ctx, t, service.WithISConfig(isCfg)) + defer tearDown() + ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + + // associate device with owner + isConn, err := grpc.NewClient(config.IDENTITY_STORE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = isConn.Close() + }() + isClient := isPb.NewIdentityStoreClient(isConn) + _, err = isClient.AddDevice(ctx, &isPb.AddDeviceRequest{ + DeviceId: deviceID, + }) + require.NoError(t, err) + + // update/create resources of the registered device + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewGrpcGatewayClient(conn) + + createResourceSub := &pb.SubscribeToEvents{ + CorrelationId: "testToken", + Action: &pb.SubscribeToEvents_CreateSubscription_{ + CreateSubscription: &pb.SubscribeToEvents_CreateSubscription{ + EventFilter: []pb.SubscribeToEvents_CreateSubscription_Event{ + pb.SubscribeToEvents_CreateSubscription_RESOURCE_CHANGED, + pb.SubscribeToEvents_CreateSubscription_RESOURCE_UPDATED, + pb.SubscribeToEvents_CreateSubscription_RESOURCE_CREATED, + pb.SubscribeToEvents_CreateSubscription_RESOURCE_DELETED, + pb.SubscribeToEvents_CreateSubscription_RESOURCE_UPDATE_PENDING, + pb.SubscribeToEvents_CreateSubscription_RESOURCE_CREATE_PENDING, + pb.SubscribeToEvents_CreateSubscription_RESOURCE_DELETE_PENDING, + }, + }, + }, + } + subClient, err := c.SubscribeToEvents(ctx) + require.NoError(t, err) + defer func() { + err2 := subClient.CloseSend() + require.NoError(t, err2) + }() + err = subClient.Send(createResourceSub) + require.NoError(t, err) + + ev, err := subClient.Recv() + require.NoError(t, err) + expectedEvent := &pb.Event{ + SubscriptionId: ev.GetSubscriptionId(), + Type: &pb.Event_OperationProcessed_{ + OperationProcessed: &pb.Event_OperationProcessed{ + ErrorStatus: &pb.Event_OperationProcessed_ErrorStatus{ + Code: pb.Event_OperationProcessed_ErrorStatus_OK, + }, + }, + }, + CorrelationId: "testToken", + } + pbTest.CmpEvent(t, expectedEvent, ev, "") + + powerTest := 654321 + _, err = c.UpdateResource(ctx, &pb.UpdateResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1")), + Content: &pb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, map[string]interface{}{ + "power": powerTest, + }), + }, + Force: true, + Async: true, + }) + require.NoError(t, err) + + _, err = c.CreateResource(ctx, &pb.CreateResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, test.TestResourceSwitchesHref), + Content: &pb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, test.MakeSwitchResourceDefaultData()), + }, + Force: true, + Async: true, + }) + require.NoError(t, err) + + _, err = c.DeleteResource(ctx, &pb.DeleteResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1")), + Force: true, + Async: true, + }) + require.NoError(t, err) + + pendingCommandsClient, err := c.GetPendingCommands(ctx, &pb.GetPendingCommandsRequest{ + DeviceIdFilter: []string{deviceID}, + IncludeHiddenResources: true, + }) + require.NoError(t, err) + numPendingCommands := 0 + for { + ev, err2 := pendingCommandsClient.Recv() + if errors.Is(err2, io.EOF) { + break + } + require.NoError(t, err2) + if ev.GetResourceCreatePending() != nil { + require.Equal(t, deviceID, ev.GetResourceCreatePending().GetResourceId().GetDeviceId()) + require.Equal(t, test.TestResourceSwitchesHref, ev.GetResourceCreatePending().GetResourceId().GetHref()) + numPendingCommands++ + } + if ev.GetResourceUpdatePending() != nil { + require.Equal(t, deviceID, ev.GetResourceUpdatePending().GetResourceId().GetDeviceId()) + switch ev.GetResourceUpdatePending().GetResourceId().GetHref() { + case test.TestResourceLightInstanceHref("1"): + numPendingCommands++ + default: + require.FailNowf(t, "unexpected pending command", "%v", ev) + } + } + if ev.GetResourceDeletePending() != nil { + require.Equal(t, deviceID, ev.GetResourceDeletePending().GetResourceId().GetDeviceId()) + require.Equal(t, test.TestResourceLightInstanceHref("1"), ev.GetResourceDeletePending().GetResourceId().GetHref()) + numPendingCommands++ + } + } + require.Equal(t, 3, numPendingCommands) + + _, shutdownDevSim := test.OnboardDevSim(ctx, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, nil) + defer shutdownDevSim() + + var lightChanged *events.ResourceChanged + var switchChanged *events.ResourceChanged + var lightUpdated *events.ResourceUpdated + var switchCreated *events.ResourceCreated + var lightDeleted *events.ResourceDeleted + for { + ev, err2 := subClient.Recv() + require.NoError(t, err2) + if ch := ev.GetResourceChanged(); ch != nil { + if ch.GetResourceId().GetHref() == test.TestResourceLightInstanceHref("1") { + d := test.DecodeCbor(t, ch.GetContent().GetData()) + if m, ok := d.(map[interface{}]interface{}); ok && m["power"] == uint64(powerTest) { + lightChanged = ch + } + } + if ch.GetResourceId().GetHref() == test.TestResourceSwitchesInstanceHref(switchID) { + switchChanged = ch + } + } + if updated := ev.GetResourceUpdated(); updated != nil { + if updated.GetResourceId().GetHref() == test.TestResourceLightInstanceHref("1") { + lightUpdated = updated + } + } + if created := ev.GetResourceCreated(); created != nil { + if created.GetResourceId().GetHref() == test.TestResourceSwitchesHref { + switchCreated = created + } + } + if deleted := ev.GetResourceDeleted(); deleted != nil { + if deleted.GetResourceId().GetHref() == test.TestResourceLightInstanceHref("1") { + lightDeleted = deleted + } + } + if lightChanged != nil && switchChanged != nil && lightUpdated != nil && switchCreated != nil && lightDeleted != nil { + break + } + } + + _, err = c.UpdateResource(ctx, &pb.UpdateResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, "/not/existing/resource"), + Content: &pb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, map[string]interface{}{ + "power": powerTest, + }), + }, + Force: true, + Async: true, + }) + require.NoError(t, err) + + pendingCommandsClient, err = c.GetPendingCommands(ctx, &pb.GetPendingCommandsRequest{ + DeviceIdFilter: []string{deviceID}, + IncludeHiddenResources: true, + }) + require.NoError(t, err) + numPendingCommands = 0 + for { + ev, err2 := pendingCommandsClient.Recv() + if errors.Is(err2, io.EOF) { + break + } + require.NoError(t, err2) + if ev.GetResourceUpdatePending() != nil { + require.Equal(t, deviceID, ev.GetResourceUpdatePending().GetResourceId().GetDeviceId()) + switch ev.GetResourceUpdatePending().GetResourceId().GetHref() { + case "/not/existing/resource": + numPendingCommands++ + default: + require.FailNowf(t, "unexpected pending command", "%v", ev) + } + } else { + require.FailNowf(t, "unexpected pending command", "%v", ev) + } + } + require.Equal(t, 1, numPendingCommands) + + _, err = c.UpdateResource(ctx, &pb.UpdateResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1")), + Content: &pb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, map[string]interface{}{ + "power": 0, + }), + }, + }) + require.NoError(t, err) +} diff --git a/grpc-gateway/test/test.go b/grpc-gateway/test/test.go index 5cc26e62c..9ac9ebba1 100644 --- a/grpc-gateway/test/test.go +++ b/grpc-gateway/test/test.go @@ -2,16 +2,37 @@ package test import ( "context" + "crypto/tls" + "crypto/x509" "sync" + "testing" "time" + "github.com/plgd-dev/hub/v2/grpc-gateway/client" "github.com/plgd-dev/hub/v2/grpc-gateway/service" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" "github.com/stretchr/testify/require" ) +func NewTestClient(t *testing.T) *client.Client { + rootCAs := x509.NewCertPool() + for _, c := range test.GetRootCertificateAuthorities(t) { + rootCAs.AddCert(c) + } + tlsCfg := tls.Config{ + RootCAs: rootCAs, + } + clientConfig := client.Config{ + GatewayAddress: config.GRPC_GW_HOST, + } + c, err := client.NewFromConfig(&clientConfig, &tlsCfg) + require.NoError(t, err) + return c +} + func MakeConfig(t require.TestingT) service.Config { var cfg service.Config diff --git a/http-gateway/config.yaml b/http-gateway/config.yaml index e48c42a8a..53f85480d 100644 --- a/http-gateway/config.yaml +++ b/http-gateway/config.yaml @@ -71,6 +71,7 @@ ui: webConfiguration: httpGatewayAddress: "" deviceProvisioningService: "" + snippetService: "" webOAuthClient: authority: "" clientID: "" diff --git a/http-gateway/service/config.go b/http-gateway/service/config.go index 2a85bfc70..7e2f206f4 100644 --- a/http-gateway/service/config.go +++ b/http-gateway/service/config.go @@ -193,6 +193,7 @@ type WebConfiguration struct { Authority string `yaml:"-" json:"authority"` HTTPGatewayAddress string `yaml:"httpGatewayAddress" json:"httpGatewayAddress"` DeviceProvisioningService string `yaml:"deviceProvisioningService" json:"deviceProvisioningService"` + SnippetService string `yaml:"snippetService" json:"snippetService"` WebOAuthClient OAuthClient `yaml:"webOAuthClient" json:"webOauthClient"` DeviceOAuthClient OAuthClient `yaml:"deviceOAuthClient" json:"deviceOauthClient"` M2MOAuthClient *OAuthClient `yaml:"m2mOAuthClient" json:"m2mOauthClient"` diff --git a/http-gateway/service/getDevicePendingCommands_test.go b/http-gateway/service/getDevicePendingCommands_test.go index 42fe7bc5c..28c906605 100644 --- a/http-gateway/service/getDevicePendingCommands_test.go +++ b/http-gateway/service/getDevicePendingCommands_test.go @@ -253,9 +253,7 @@ func TestRequestHandlerGetDevicePendingCommands(t *testing.T) { secureGWShutdown() createFn := func() { - createCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errC := c.CreateResource(createCtx, &pb.CreateResourceRequest{ + _, errC := c.CreateResource(ctx, &pb.CreateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, device.ResourceURI), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -263,8 +261,9 @@ func TestRequestHandlerGetDevicePendingCommands(t *testing.T) { "power": 1, }), }, + Async: true, }) - require.Error(t, errC) + require.NoError(t, errC) } createFn() retrieveFn := func() { @@ -277,9 +276,7 @@ func TestRequestHandlerGetDevicePendingCommands(t *testing.T) { } retrieveFn() updateFn := func() { - updateCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errU := c.UpdateResource(updateCtx, &pb.UpdateResourceRequest{ + _, errU := c.UpdateResource(ctx, &pb.UpdateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1")), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -287,17 +284,17 @@ func TestRequestHandlerGetDevicePendingCommands(t *testing.T) { "power": 1, }), }, + Async: true, }) - require.Error(t, errU) + require.NoError(t, errU) } updateFn() deleteFn := func() { - deleteCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errD := c.DeleteResource(deleteCtx, &pb.DeleteResourceRequest{ + _, errD := c.DeleteResource(ctx, &pb.DeleteResourceRequest{ ResourceId: commands.NewResourceID(deviceID, device.ResourceURI), + Async: true, }) - require.Error(t, errD) + require.NoError(t, errD) } deleteFn() updateDeviceMetadataFn := func() { diff --git a/http-gateway/service/getDeviceResourceLinks_test.go b/http-gateway/service/getDeviceResourceLinks_test.go index a7a71b7e1..13a8a650c 100644 --- a/http-gateway/service/getDeviceResourceLinks_test.go +++ b/http-gateway/service/getDeviceResourceLinks_test.go @@ -6,6 +6,7 @@ import ( "errors" "io" "net/http" + "slices" "testing" "time" @@ -16,7 +17,6 @@ import ( httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" - "github.com/plgd-dev/hub/v2/pkg/strings" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" test "github.com/plgd-dev/hub/v2/test" @@ -100,8 +100,8 @@ func TestRequestHandlerGetDeviceResourceLinks(t *testing.T) { { DeviceId: deviceID, Resources: test.ResourceLinksToResources(deviceID, test.FilterResourceLink(func(rl schema.ResourceLink) bool { - return strings.Contains(rl.ResourceTypes, collection.ResourceType) || - strings.Contains(rl.ResourceTypes, types.BINARY_SWITCH) + return slices.Contains(rl.ResourceTypes, collection.ResourceType) || + slices.Contains(rl.ResourceTypes, types.BINARY_SWITCH) }, resourceLinks)), AuditContext: commands.NewAuditContext(oauthService.DeviceUserID, "", oauthService.DeviceUserID), }, diff --git a/http-gateway/service/getHubConfiguration.go b/http-gateway/service/getHubConfiguration.go index 6467821b4..c693de243 100644 --- a/http-gateway/service/getHubConfiguration.go +++ b/http-gateway/service/getHubConfiguration.go @@ -75,6 +75,7 @@ func (requestHandler *RequestHandler) getHubConfiguration(w http.ResponseWriter, resp.Ui = &pb.UIConfiguration{ Visibility: requestHandler.config.UI.WebConfiguration.Visibility.ToProto(), DeviceProvisioningService: requestHandler.config.UI.WebConfiguration.DeviceProvisioningService, + SnippetService: requestHandler.config.UI.WebConfiguration.SnippetService, } if accept == pkgHttp.ApplicationProtoJsonContentType { m := serverMux.NewJsonpbMarshaler() diff --git a/http-gateway/service/getHubConfiguration_test.go b/http-gateway/service/getHubConfiguration_test.go index 351500f83..fba747935 100644 --- a/http-gateway/service/getHubConfiguration_test.go +++ b/http-gateway/service/getHubConfiguration_test.go @@ -30,6 +30,7 @@ func TestRequestHandlerGetHubConfiguration(t *testing.T) { expected.Ui = &pb.UIConfiguration{ Visibility: httpCfg.UI.WebConfiguration.Visibility.ToProto(), DeviceProvisioningService: httpCfg.UI.WebConfiguration.DeviceProvisioningService, + SnippetService: httpCfg.UI.WebConfiguration.SnippetService, } tests := []struct { name string @@ -85,6 +86,7 @@ func TestRequestHandlerGetHubConfigurationWithoutM2MOAuthClient(t *testing.T) { expected.Ui = &pb.UIConfiguration{ Visibility: httpCfg.UI.WebConfiguration.Visibility.ToProto(), DeviceProvisioningService: httpCfg.UI.WebConfiguration.DeviceProvisioningService, + SnippetService: httpCfg.UI.WebConfiguration.SnippetService, } tests := []struct { name string diff --git a/http-gateway/service/getPendingCommands_test.go b/http-gateway/service/getPendingCommands_test.go index d790d18a3..a39ca1c2f 100644 --- a/http-gateway/service/getPendingCommands_test.go +++ b/http-gateway/service/getPendingCommands_test.go @@ -300,9 +300,7 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { secureGWShutdown() createFn := func() { - createCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errC := c.CreateResource(createCtx, &pb.CreateResourceRequest{ + _, errC := c.CreateResource(ctx, &pb.CreateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, device.ResourceURI), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -310,8 +308,9 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { "power": 1, }), }, + Async: true, }) - require.Error(t, errC) + require.NoError(t, errC) } createFn() retrieveFn := func() { @@ -324,9 +323,7 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { } retrieveFn() updateFn := func() { - updateCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errU := c.UpdateResource(updateCtx, &pb.UpdateResourceRequest{ + _, errU := c.UpdateResource(ctx, &pb.UpdateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1")), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -334,17 +331,17 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { "power": 1, }), }, + Async: true, }) - require.Error(t, errU) + require.NoError(t, errU) } updateFn() deleteFn := func() { - deleteCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errD := c.DeleteResource(deleteCtx, &pb.DeleteResourceRequest{ + _, errD := c.DeleteResource(ctx, &pb.DeleteResourceRequest{ ResourceId: commands.NewResourceID(deviceID, device.ResourceURI), + Async: true, }) - require.Error(t, errD) + require.NoError(t, errD) } deleteFn() updateDeviceMetadataFn := func() { diff --git a/http-gateway/service/getResourcePendingCommands_test.go b/http-gateway/service/getResourcePendingCommands_test.go index 7d66de5d1..d75438626 100644 --- a/http-gateway/service/getResourcePendingCommands_test.go +++ b/http-gateway/service/getResourcePendingCommands_test.go @@ -165,9 +165,7 @@ func TestRequestHandlerGetResourcePendingCommands(t *testing.T) { secureGWShutdown() createFn := func() { - createCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errC := c.CreateResource(createCtx, &pb.CreateResourceRequest{ + _, errC := c.CreateResource(ctx, &pb.CreateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, device.ResourceURI), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -175,8 +173,9 @@ func TestRequestHandlerGetResourcePendingCommands(t *testing.T) { "power": 1, }), }, + Async: true, }) - require.Error(t, errC) + require.NoError(t, errC) } createFn() retrieveFn := func() { @@ -189,9 +188,7 @@ func TestRequestHandlerGetResourcePendingCommands(t *testing.T) { } retrieveFn() updateFn := func() { - updateCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errU := c.UpdateResource(updateCtx, &pb.UpdateResourceRequest{ + _, errU := c.UpdateResource(ctx, &pb.UpdateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1")), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -199,17 +196,17 @@ func TestRequestHandlerGetResourcePendingCommands(t *testing.T) { "power": 1, }), }, + Async: true, }) - require.Error(t, errU) + require.NoError(t, errU) } updateFn() deleteFn := func() { - deleteCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errD := c.DeleteResource(deleteCtx, &pb.DeleteResourceRequest{ + _, errD := c.DeleteResource(ctx, &pb.DeleteResourceRequest{ ResourceId: commands.NewResourceID(deviceID, device.ResourceURI), + Async: true, }) - require.Error(t, errD) + require.NoError(t, errD) } deleteFn() updateDeviceMetadataFn := func() { diff --git a/http-gateway/test/test.go b/http-gateway/test/test.go index a15981d17..0019d542b 100644 --- a/http-gateway/test/test.go +++ b/http-gateway/test/test.go @@ -21,6 +21,7 @@ func MakeWebConfigurationConfig() service.WebConfiguration { Authority: testHttp.HTTPS_SCHEME + config.OAUTH_SERVER_HOST, HTTPGatewayAddress: testHttp.HTTPS_SCHEME + config.HTTP_GW_HOST, DeviceProvisioningService: testHttp.HTTPS_SCHEME + config.HTTP_GW_HOST, + SnippetService: testHttp.HTTPS_SCHEME + config.SNIPPET_SERVICE_HTTP_HOST, WebOAuthClient: service.OAuthClient{ Authority: testHttp.HTTPS_SCHEME + config.OAUTH_SERVER_HOST, ClientID: config.OAUTH_MANAGER_CLIENT_ID, @@ -51,7 +52,7 @@ func MakeConfig(t require.TestingT, enableUI bool) service.Config { cfg.Log = log.MakeDefaultConfig() - cfg.APIs.HTTP.Authorization = config.MakeAuthorizationConfig() + cfg.APIs.HTTP.Authorization = config.MakeValidatorConfig() cfg.APIs.HTTP.Connection = config.MakeListenerConfig(config.HTTP_GW_HOST) cfg.APIs.HTTP.Connection.TLS.ClientCertificateRequired = false cfg.APIs.HTTP.WebSocket.StreamBodyLimit = 256 * 1024 diff --git a/http-gateway/uri/uri.go b/http-gateway/uri/uri.go index 0a4fd3323..05aba37dd 100644 --- a/http-gateway/uri/uri.go +++ b/http-gateway/uri/uri.go @@ -7,21 +7,23 @@ const ( ResourceHrefKey = "resourceHref" CorrelationIDKey = "correlationId" - ResourceInterfaceQueryKey = "resourceInterface" - TwinQueryKey = "twin" - CommandFilterQueryKey = "commandFilter" - TypeFilterQueryKey = "typeFilter" - StatusFilterQueryKey = "statusFilter" - DeviceIdFilterQueryKey = "deviceIdFilter" - TimeToLiveQueryKey = "timeToLive" - ResourceIdFilterQueryKey = "resourceIdFilter" - HttpResourceIdFilterQueryKey = "httpResourceIdFilter" - AcceptQueryKey = "accept" // for websocket - CorrelationIDQueryKey = "correlationId" - TimestampFilterQueryKey = "timestampFilter" - CorrelationIdFilterQueryKey = "correlationIdFilter" - ETagQueryKey = "etag" - OnlyContentQueryKey = "onlyContent" + ResourceInterfaceQueryKey = "resourceInterface" + TwinQueryKey = "twin" + CommandFilterQueryKey = "commandFilter" + TypeFilterQueryKey = "typeFilter" + StatusFilterQueryKey = "statusFilter" + DeviceIdFilterQueryKey = "deviceIdFilter" + TimeToLiveQueryKey = "timeToLive" + ResourceIdFilterQueryKey = "resourceIdFilter" + HttpResourceIdFilterQueryKey = "httpResourceIdFilter" + AcceptQueryKey = "accept" // for websocket + CorrelationIDQueryKey = "correlationId" + TimestampFilterQueryKey = "timestampFilter" + CorrelationIdFilterQueryKey = "correlationIdFilter" + ETagQueryKey = "etag" + OnlyContentQueryKey = "onlyContent" + IncludeHiddenResourcesQueryKey = "includeHiddenResources" + ForceQueryKey = "force" AliasInterfaceQueryKey = "interface" AliasCommandFilterQueryKey = "command" @@ -116,23 +118,25 @@ const ( ) var QueryCaseInsensitive = map[string]string{ - strings.ToLower(AliasInterfaceQueryKey): ResourceInterfaceQueryKey, - strings.ToLower(CommandFilterQueryKey): CommandFilterQueryKey, - strings.ToLower(DeviceIdFilterQueryKey): DeviceIdFilterQueryKey, - strings.ToLower(ResourceIdFilterQueryKey): HttpResourceIdFilterQueryKey, - strings.ToLower(ResourceInterfaceQueryKey): ResourceInterfaceQueryKey, - strings.ToLower(TwinQueryKey): TwinQueryKey, - strings.ToLower(TypeFilterQueryKey): TypeFilterQueryKey, - strings.ToLower(AliasCommandFilterQueryKey): CommandFilterQueryKey, - strings.ToLower(AliasDeviceIdFilterQueryKey): DeviceIdFilterQueryKey, - strings.ToLower(AliasResourceIdFilterQueryKey): HttpResourceIdFilterQueryKey, - strings.ToLower(AliasTypeFilterQueryKey): TypeFilterQueryKey, - strings.ToLower(AcceptQueryKey): AcceptQueryKey, - strings.ToLower(StatusFilterQueryKey): StatusFilterQueryKey, - strings.ToLower(AliasStatusFilterQueryKey): StatusFilterQueryKey, - strings.ToLower(CorrelationIDQueryKey): CorrelationIDQueryKey, - strings.ToLower(TimestampFilterQueryKey): TimestampFilterQueryKey, - strings.ToLower(TimeToLiveQueryKey): TimeToLiveQueryKey, - strings.ToLower(CorrelationIdFilterQueryKey): CorrelationIdFilterQueryKey, - strings.ToLower(OnlyContentQueryKey): OnlyContentQueryKey, + strings.ToLower(AliasInterfaceQueryKey): ResourceInterfaceQueryKey, + strings.ToLower(CommandFilterQueryKey): CommandFilterQueryKey, + strings.ToLower(DeviceIdFilterQueryKey): DeviceIdFilterQueryKey, + strings.ToLower(ResourceIdFilterQueryKey): HttpResourceIdFilterQueryKey, + strings.ToLower(ResourceInterfaceQueryKey): ResourceInterfaceQueryKey, + strings.ToLower(TwinQueryKey): TwinQueryKey, + strings.ToLower(TypeFilterQueryKey): TypeFilterQueryKey, + strings.ToLower(AliasCommandFilterQueryKey): CommandFilterQueryKey, + strings.ToLower(AliasDeviceIdFilterQueryKey): DeviceIdFilterQueryKey, + strings.ToLower(AliasResourceIdFilterQueryKey): HttpResourceIdFilterQueryKey, + strings.ToLower(AliasTypeFilterQueryKey): TypeFilterQueryKey, + strings.ToLower(AcceptQueryKey): AcceptQueryKey, + strings.ToLower(StatusFilterQueryKey): StatusFilterQueryKey, + strings.ToLower(AliasStatusFilterQueryKey): StatusFilterQueryKey, + strings.ToLower(CorrelationIDQueryKey): CorrelationIDQueryKey, + strings.ToLower(TimestampFilterQueryKey): TimestampFilterQueryKey, + strings.ToLower(TimeToLiveQueryKey): TimeToLiveQueryKey, + strings.ToLower(CorrelationIdFilterQueryKey): CorrelationIdFilterQueryKey, + strings.ToLower(OnlyContentQueryKey): OnlyContentQueryKey, + strings.ToLower(IncludeHiddenResourcesQueryKey): IncludeHiddenResourcesQueryKey, + strings.ToLower(ForceQueryKey): ForceQueryKey, } diff --git a/http-gateway/web/src/containers/PendingCommands/hooks.ts b/http-gateway/web/src/containers/PendingCommands/hooks.ts index 712490aff..3620229cd 100644 --- a/http-gateway/web/src/containers/PendingCommands/hooks.ts +++ b/http-gateway/web/src/containers/PendingCommands/hooks.ts @@ -11,7 +11,7 @@ import { SecurityConfig, StreamApiPropsType } from '@/containers/App/App.types' const getConfig = () => security.getGeneralConfig() as SecurityConfig export const usePendingCommandsList = (deviceId?: string) => { - const filter = deviceId ? `?deviceIdFilter=${deviceId}` : '' + const filter = deviceId ? `?deviceIdFilter=${deviceId}&includeHiddenResources=true` : `?includeHiddenResources=true` const { telemetryWebTracer, unauthorizedCallback } = useContext(AppContext) const { data, updateData, ...rest }: StreamApiPropsType = useStreamApi( `${getConfig().httpGatewayAddress}${pendingCommandsApiEndpoints.PENDING_COMMANDS}${filter}`, diff --git a/identity-store/service/service_test.go b/identity-store/service/service_test.go index fdd330898..30c9a50f8 100644 --- a/identity-store/service/service_test.go +++ b/identity-store/service/service_test.go @@ -34,7 +34,7 @@ func MakeConfig(t require.TestingT) Config { cfg.APIs.GRPC.TLS.CertFile = config.CERT_FILE cfg.APIs.GRPC.TLS.KeyFile = config.KEY_FILE cfg.APIs.GRPC.Authorization.OwnerClaim = config.OWNER_CLAIM - cfg.APIs.GRPC.Authorization.Config = config.MakeAuthorizationConfig() + cfg.APIs.GRPC.Authorization.Config = config.MakeValidatorConfig() cfg.HubID = config.HubID() diff --git a/m2m-oauth-server/test/test.go b/m2m-oauth-server/test/test.go index fedfbb89c..91c5da67d 100644 --- a/m2m-oauth-server/test/test.go +++ b/m2m-oauth-server/test/test.go @@ -51,7 +51,7 @@ var JWTPrivateKeyOAuthClient = service.Client{ AllowedScopes: nil, JWTPrivateKey: service.PrivateKeyJWTConfig{ Enabled: true, - Authorization: config.MakeAuthorizationConfig(), + Authorization: config.MakeValidatorConfig(), }, } diff --git a/pkg/mongodb/marshal.go b/pkg/mongodb/marshal.go new file mode 100644 index 000000000..54dbd8acb --- /dev/null +++ b/pkg/mongodb/marshal.go @@ -0,0 +1,89 @@ +package mongodb + +import ( + "encoding/json" + "strconv" + "strings" + + "go.mongodb.org/mongo-driver/bson" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +type updateJSON = func(map[string]interface{}) + +func ConvertStringValueToInt64(json map[string]interface{}, path string) { + pos := strings.Index(path, ".") + if pos == -1 { + valueI, ok := json[path] + if !ok { + return + } + valueStr, ok := valueI.(string) + if !ok { + return + } + value, err := strconv.ParseInt(valueStr, 10, 64) + if err != nil { + return + } + json[path] = value + return + } + + elemPath := path[:pos] + elem, ok := json[elemPath] + if !ok { + return + } + elemArray, ok := elem.([]interface{}) + if ok { + for i, elem := range elemArray { + elemMap, ok2 := elem.(map[string]interface{}) + if !ok2 { + continue + } + ConvertStringValueToInt64(elemMap, path[pos+1:]) + elemArray[i] = elemMap + } + json[elemPath] = elemArray + return + } + elemMap, ok := elem.(map[string]interface{}) + if !ok { + return + } + ConvertStringValueToInt64(elemMap, path[pos+1:]) + json[elemPath] = elemMap +} + +func UnmarshalProtoBSON(data []byte, m proto.Message, update updateJSON) error { + var obj map[string]interface{} + if err := bson.Unmarshal(data, &obj); err != nil { + return err + } + if update != nil { + update(obj) + } + jsonData, err := json.Marshal(obj) + if err != nil { + return err + } + return protojson.Unmarshal(jsonData, m) +} + +func MarshalProtoBSON(m proto.Message, update updateJSON) ([]byte, error) { + data, err := protojson.Marshal(m) + if err != nil { + return nil, err + } + var obj map[string]interface{} + err = json.Unmarshal(data, &obj) + if err != nil { + return nil, err + } + if update != nil { + update(obj) + } + return bson.Marshal(obj) +} diff --git a/pkg/mongodb/mongo.go b/pkg/mongodb/mongo.go new file mode 100644 index 000000000..757ee80f4 --- /dev/null +++ b/pkg/mongodb/mongo.go @@ -0,0 +1,14 @@ +package mongodb + +type Operator = string + +const ( + And Operator = "$and" + Or Operator = "$or" + Exists Operator = "$exists" + All Operator = "$all" + In Operator = "$in" + Set Operator = "$set" + Unset Operator = "$unset" + Match Operator = "$match" +) diff --git a/pkg/net/grpc/server/newServer.go b/pkg/net/grpc/server/newServer.go index ff2d275d9..3a1cb2ec5 100644 --- a/pkg/net/grpc/server/newServer.go +++ b/pkg/net/grpc/server/newServer.go @@ -1,8 +1,10 @@ package server import ( + "errors" "fmt" "net" + "sync/atomic" "github.com/plgd-dev/hub/v2/pkg/fn" "google.golang.org/grpc" @@ -14,6 +16,7 @@ type Server struct { listener net.Listener gracefulStop bool closeFunc fn.FuncList + serving atomic.Bool } // NewServer instantiates a gRPC server. @@ -44,6 +47,9 @@ func (s *Server) Addr() string { // Serve starts serving and blocks. func (s *Server) Serve() error { + if !s.serving.CompareAndSwap(false, true) { + return errors.New("already serving") + } err := s.Server.Serve(s.listener) if err != nil { return fmt.Errorf("serving failed: %w", err) @@ -57,11 +63,16 @@ func (s *Server) Serve() error { // pending RPCs on the client side will get notified by connection // errors. func (s *Server) Close() error { - if s.gracefulStop { - s.Server.GracefulStop() + var err error + if !s.serving.Load() { + err = s.listener.Close() } else { - s.Server.Stop() + if s.gracefulStop { + s.Server.GracefulStop() + } else { + s.Server.Stop() + } } s.closeFunc.Execute() - return nil + return err } diff --git a/pkg/net/http/service/service.go b/pkg/net/http/service/service.go index 42b3aedc0..d23257265 100644 --- a/pkg/net/http/service/service.go +++ b/pkg/net/http/service/service.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "sync/atomic" "github.com/gorilla/mux" "github.com/plgd-dev/hub/v2/http-gateway/serverMux" @@ -18,6 +19,7 @@ type Service struct { config *Config listener *listener.Server router *mux.Router + serving atomic.Bool } // New parses configuration and creates new http service @@ -52,6 +54,9 @@ func New(config Config) (*Service, error) { // Serve starts the service's HTTP server and blocks func (s *Service) Serve() error { + if !s.serving.CompareAndSwap(false, true) { + return errors.New("service is already serving") + } err := s.server.Serve(s.listener) if errors.Is(err, http.ErrServerClosed) { return nil @@ -61,6 +66,9 @@ func (s *Service) Serve() error { // Close ends serving func (s *Service) Close() error { + if !s.serving.Load() { + return s.listener.Close() + } return s.server.Shutdown(context.Background()) } diff --git a/pkg/service/service.go b/pkg/service/service.go index 3d58cbb64..83a10a1d8 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -94,6 +94,7 @@ func (s *Service) SigTerm() { func (s *Service) Close() error { s.SigTerm() if !s.serving.Load() { + s.closeFn.Execute() return nil } <-s.done diff --git a/pkg/strings/slice.go b/pkg/strings/slice.go index 264e4d171..c754aea59 100644 --- a/pkg/strings/slice.go +++ b/pkg/strings/slice.go @@ -1,6 +1,9 @@ package strings -import "errors" +import ( + "errors" + "slices" +) var ErrInvalidType = errors.New("invalid type") @@ -51,27 +54,8 @@ func Unique(s []string) []string { return nil } - set := make(map[string]struct{}) - for _, v := range s { - set[v] = struct{}{} - } - - keys := make([]string, len(set)) - i := 0 - for k := range set { - keys[i] = k - i++ - } - return keys -} - -func Contains(slice []string, s string) bool { - for _, v := range slice { - if v == s { - return true - } - } - return false + slices.Sort(s) + return slices.Compact(s) } // ToSlice converts a string or a []string. diff --git a/pkg/strings/slice_test.go b/pkg/strings/slice_test.go index 6fee31e9f..dd1d021d3 100644 --- a/pkg/strings/slice_test.go +++ b/pkg/strings/slice_test.go @@ -173,49 +173,6 @@ func TestUnique(t *testing.T) { } } -func TestContains(t *testing.T) { - type args struct { - slice []string - s string - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "Empty", - args: args{ - slice: nil, - s: "a", - }, - want: false, - }, - { - name: "Not found", - args: args{ - slice: []string{"a", "bb", "ccc"}, - s: "b", - }, - want: false, - }, - { - name: "Found", - args: args{ - slice: []string{"a", "bb", "ccc"}, - s: "bb", - }, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := Contains(tt.args.slice, tt.args.s) - require.Equal(t, tt.want, got) - }) - } -} - func TestToSlice(t *testing.T) { s, err := ToSlice(nil) require.NoError(t, err) diff --git a/resource-aggregate/commands/commands.pb.go b/resource-aggregate/commands/commands.pb.go index 07df1d8b8..8b6a0ee9d 100644 --- a/resource-aggregate/commands/commands.pb.go +++ b/resource-aggregate/commands/commands.pb.go @@ -733,6 +733,7 @@ type UpdateResourceRequest struct { ResourceInterface string `protobuf:"bytes,4,opt,name=resource_interface,json=resourceInterface,proto3" json:"resource_interface,omitempty"` TimeToLive int64 `protobuf:"varint,5,opt,name=time_to_live,json=timeToLive,proto3" json:"time_to_live,omitempty"` // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). CommandMetadata *CommandMetadata `protobuf:"bytes,6,opt,name=command_metadata,json=commandMetadata,proto3" json:"command_metadata,omitempty"` + Force bool `protobuf:"varint,7,opt,name=force,proto3" json:"force,omitempty"` // if true, the command will be executed even if the resource does not exist } func (x *UpdateResourceRequest) Reset() { @@ -809,6 +810,13 @@ func (x *UpdateResourceRequest) GetCommandMetadata() *CommandMetadata { return nil } +func (x *UpdateResourceRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + type UpdateResourceResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1276,6 +1284,7 @@ type DeleteResourceRequest struct { TimeToLive int64 `protobuf:"varint,3,opt,name=time_to_live,json=timeToLive,proto3" json:"time_to_live,omitempty"` // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). CommandMetadata *CommandMetadata `protobuf:"bytes,4,opt,name=command_metadata,json=commandMetadata,proto3" json:"command_metadata,omitempty"` ResourceInterface string `protobuf:"bytes,5,opt,name=resource_interface,json=resourceInterface,proto3" json:"resource_interface,omitempty"` + Force bool `protobuf:"varint,6,opt,name=force,proto3" json:"force,omitempty"` // if true, the command will be executed even if the resource does not exist } func (x *DeleteResourceRequest) Reset() { @@ -1345,6 +1354,13 @@ func (x *DeleteResourceRequest) GetResourceInterface() string { return "" } +func (x *DeleteResourceRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + type DeleteResourceResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1536,6 +1552,7 @@ type CreateResourceRequest struct { Content *Content `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` TimeToLive int64 `protobuf:"varint,4,opt,name=time_to_live,json=timeToLive,proto3" json:"time_to_live,omitempty"` // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). CommandMetadata *CommandMetadata `protobuf:"bytes,5,opt,name=command_metadata,json=commandMetadata,proto3" json:"command_metadata,omitempty"` + Force bool `protobuf:"varint,6,opt,name=force,proto3" json:"force,omitempty"` // if true, the command will be executed even if the resource does not exist } func (x *CreateResourceRequest) Reset() { @@ -1605,6 +1622,13 @@ func (x *CreateResourceRequest) GetCommandMetadata() *CommandMetadata { return nil } +func (x *CreateResourceRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + type CreateResourceResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2949,7 +2973,7 @@ var file_resource_aggregate_pb_commands_proto_rawDesc = []byte{ 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, - 0x74, 0x65, 0x78, 0x74, 0x22, 0xdd, 0x02, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x74, 0x65, 0x78, 0x74, 0x22, 0xf3, 0x02, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, @@ -2971,7 +2995,64 @@ var file_resource_aggregate_pb_commands_proto_rawDesc = []byte{ 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x22, 0x82, 0x01, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x22, 0x82, 0x01, 0x0a, 0x16, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, + 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, + 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, + 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, + 0xc9, 0x02, 0x0a, 0x1c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, + 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, + 0x62, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x37, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, + 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, + 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, + 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x68, 0x0a, 0x1d, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0d, + 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, + 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, + 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xba, 0x02, 0x0a, 0x17, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, + 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, + 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x69, + 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x50, 0x0a, 0x10, + 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, + 0x0a, 0x04, 0x65, 0x74, 0x61, 0x67, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x65, 0x74, + 0x61, 0x67, 0x22, 0x84, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, @@ -2979,165 +3060,56 @@ var file_resource_aggregate_pb_commands_proto_rawDesc = []byte{ 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, - 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xc9, 0x02, 0x0a, 0x1c, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, - 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, - 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, - 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, - 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x37, 0x0a, 0x07, 0x63, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, - 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, - 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x68, 0x0a, 0x1d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, - 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, + 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xdf, 0x02, 0x0a, 0x1e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x74, + 0x72, 0x69, 0x65, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, + 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, + 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x37, 0x0a, 0x07, + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, - 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, - 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, - 0xba, 0x02, 0x0a, 0x17, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, - 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, - 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x66, 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, - 0x6c, 0x69, 0x76, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, - 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, - 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x65, 0x74, 0x61, 0x67, - 0x18, 0x06, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x65, 0x74, 0x61, 0x67, 0x22, 0x84, 0x01, 0x0a, - 0x18, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x61, 0x6c, - 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, - 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, - 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, - 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, - 0x65, 0x78, 0x74, 0x22, 0xdf, 0x02, 0x0a, 0x1e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, - 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x52, 0x0a, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, - 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, - 0x12, 0x34, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, - 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x37, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x65, 0x74, 0x61, 0x67, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x65, 0x74, 0x61, 0x67, 0x22, 0x6a, 0x0a, 0x1f, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, + 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, + 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, + 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xba, 0x02, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, + 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, + 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x50, 0x0a, + 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, - 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, - 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x12, 0x12, 0x0a, 0x04, 0x65, 0x74, 0x61, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x04, 0x65, 0x74, 0x61, 0x67, 0x22, 0x6a, 0x0a, 0x1f, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, - 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, - 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, - 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, - 0x74, 0x22, 0xa4, 0x02, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, - 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, - 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, - 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x69, 0x6d, - 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, - 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, - 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x22, 0x82, 0x01, 0x0a, 0x16, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, - 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, - 0x6e, 0x74, 0x69, 0x6c, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, - 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, - 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, - 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xc9, 0x02, - 0x0a, 0x1c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, - 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, - 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, - 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x37, - 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, - 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x07, - 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, - 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x68, 0x0a, 0x1d, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, - 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, - 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, - 0x65, 0x78, 0x74, 0x22, 0xae, 0x02, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, - 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, - 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, - 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x37, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, - 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, - 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, - 0x76, 0x65, 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, - 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x22, 0x82, 0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, + 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, + 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, + 0x6f, 0x72, 0x63, 0x65, 0x22, 0x82, 0x01, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, @@ -3146,8 +3118,8 @@ var file_resource_aggregate_pb_commands_proto_rawDesc = []byte{ 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xc9, 0x02, 0x0a, 0x1c, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, + 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, @@ -3167,217 +3139,273 @@ var file_resource_aggregate_pb_commands_proto_rawDesc = []byte{ 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x68, 0x0a, 0x1d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, - 0xa0, 0x03, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3f, - 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, + 0xc4, 0x02, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, - 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, - 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, - 0x41, 0x74, 0x12, 0x45, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, - 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, - 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x6c, 0x6f, 0x63, 0x61, - 0x6c, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x73, 0x22, 0x21, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x4f, - 0x46, 0x46, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4f, 0x4e, 0x4c, 0x49, - 0x4e, 0x45, 0x10, 0x01, 0x22, 0x52, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, - 0x04, 0x43, 0x4f, 0x41, 0x50, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4f, 0x41, 0x50, 0x53, - 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x4f, 0x41, 0x50, 0x5f, 0x54, 0x43, 0x50, 0x10, 0x03, - 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x41, 0x50, 0x53, 0x5f, 0x54, 0x43, 0x50, 0x10, 0x04, 0x12, - 0x07, 0x0a, 0x03, 0x43, 0x32, 0x43, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x12, - 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, - 0x69, 0x6c, 0x22, 0xe7, 0x02, 0x0a, 0x13, 0x54, 0x77, 0x69, 0x6e, 0x53, 0x79, 0x6e, 0x63, 0x68, - 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x45, 0x0a, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2f, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, - 0x2e, 0x54, 0x77, 0x69, 0x6e, 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x41, 0x74, - 0x12, 0x1c, 0x0a, 0x0a, 0x69, 0x6e, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x61, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x6e, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x74, 0x12, 0x50, + 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, + 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, + 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x12, 0x37, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, + 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0c, + 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x12, 0x38, 0x0a, 0x18, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, - 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x16, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, - 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x22, 0x40, 0x0a, 0x05, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x4f, 0x55, 0x54, 0x5f, 0x4f, 0x46, 0x5f, 0x53, 0x59, - 0x4e, 0x43, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, - 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x59, 0x4e, 0x43, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, - 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x10, 0x03, 0x22, 0xee, 0x03, 0x0a, - 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, - 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, - 0x12, 0x42, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, - 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5e, 0x0a, 0x14, 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x73, 0x79, 0x6e, - 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, - 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x77, 0x69, 0x6e, 0x53, 0x79, - 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, - 0x13, 0x74, 0x77, 0x69, 0x6e, 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0c, 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x65, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0b, 0x74, 0x77, - 0x69, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x3e, 0x0a, 0x1a, 0x74, 0x77, 0x69, - 0x6e, 0x5f, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, - 0x18, 0x74, 0x77, 0x69, 0x6e, 0x46, 0x6f, 0x72, 0x63, 0x65, 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, - 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x69, 0x6d, - 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x50, 0x0a, 0x10, 0x63, - 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x08, 0x0a, - 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0xab, 0x01, - 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, - 0x0a, 0x0b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 0x12, - 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, - 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, - 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x77, 0x69, 0x6e, - 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, - 0x74, 0x77, 0x69, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xe6, 0x02, 0x0a, 0x22, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, + 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x22, 0x82, 0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, 0x6c, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, + 0x69, 0x6c, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, + 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, + 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xc9, 0x02, 0x0a, 0x1c, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, + 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, 0x0a, 0x0c, - 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x08, 0x48, 0x00, 0x52, 0x0b, 0x74, 0x77, 0x69, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x12, 0x3e, 0x0a, 0x1a, 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, - 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x18, 0x74, 0x77, 0x69, 0x6e, 0x46, 0x6f, 0x72, - 0x63, 0x65, 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x37, 0x0a, 0x07, + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, + 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x68, 0x0a, 0x1d, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x72, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, + 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, + 0x74, 0x22, 0xa0, 0x03, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x3f, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, + 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x65, 0x64, 0x41, 0x74, 0x12, 0x45, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, + 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x6c, 0x6f, + 0x63, 0x61, 0x6c, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x73, 0x22, 0x21, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, + 0x07, 0x4f, 0x46, 0x46, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4f, 0x4e, + 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x01, 0x22, 0x52, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, + 0x08, 0x0a, 0x04, 0x43, 0x4f, 0x41, 0x50, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4f, 0x41, + 0x50, 0x53, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x4f, 0x41, 0x50, 0x5f, 0x54, 0x43, 0x50, + 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x41, 0x50, 0x53, 0x5f, 0x54, 0x43, 0x50, 0x10, + 0x04, 0x12, 0x07, 0x0a, 0x03, 0x43, 0x32, 0x43, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, + 0x52, 0x12, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, + 0x6e, 0x74, 0x69, 0x6c, 0x22, 0xe7, 0x02, 0x0a, 0x13, 0x54, 0x77, 0x69, 0x6e, 0x53, 0x79, 0x6e, + 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x45, 0x0a, 0x05, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, - 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x4a, 0x04, - 0x08, 0x04, 0x10, 0x05, 0x22, 0x6e, 0x0a, 0x23, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x0d, 0x61, - 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x70, 0x62, 0x2e, 0x54, 0x77, 0x69, 0x6e, 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x61, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e, 0x67, + 0x41, 0x74, 0x12, 0x1c, 0x0a, 0x0a, 0x69, 0x6e, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x61, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x6e, 0x53, 0x79, 0x6e, 0x63, 0x41, 0x74, + 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, + 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x38, 0x0a, 0x18, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x79, 0x6e, 0x63, + 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x74, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x16, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x53, 0x79, 0x6e, 0x63, 0x68, + 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x22, 0x40, 0x0a, 0x05, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x4f, 0x55, 0x54, 0x5f, 0x4f, 0x46, 0x5f, + 0x53, 0x59, 0x4e, 0x43, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, + 0x45, 0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x59, 0x4e, 0x43, 0x49, 0x4e, 0x47, 0x10, + 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x10, 0x03, 0x22, 0xee, + 0x03, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, + 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, + 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x49, 0x64, 0x12, 0x42, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5e, 0x0a, 0x14, 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x73, + 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, + 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x77, 0x69, 0x6e, + 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, + 0x00, 0x52, 0x13, 0x74, 0x77, 0x69, 0x6e, 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0c, 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0b, + 0x74, 0x77, 0x69, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x3e, 0x0a, 0x1a, 0x74, + 0x77, 0x69, 0x6e, 0x5f, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, + 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x48, + 0x00, 0x52, 0x18, 0x74, 0x77, 0x69, 0x6e, 0x46, 0x6f, 0x72, 0x63, 0x65, 0x53, 0x79, 0x6e, 0x63, + 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0c, 0x74, + 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x50, 0x0a, + 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, + 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, + 0x08, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, + 0xab, 0x01, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, + 0x6c, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, + 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x77, + 0x69, 0x6e, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0b, 0x74, 0x77, 0x69, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xe6, 0x02, + 0x0a, 0x22, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, + 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, + 0x0a, 0x0c, 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0b, 0x74, 0x77, 0x69, 0x6e, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x12, 0x3e, 0x0a, 0x1a, 0x74, 0x77, 0x69, 0x6e, 0x5f, 0x66, 0x6f, 0x72, 0x63, + 0x65, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x18, 0x74, 0x77, 0x69, 0x6e, 0x46, + 0x6f, 0x72, 0x63, 0x65, 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x50, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, + 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, + 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x6e, 0x0a, 0x23, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, + 0x6d, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, + 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, + 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, + 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, + 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xe7, 0x01, 0x0a, 0x1c, 0x43, 0x61, 0x6e, 0x63, 0x65, + 0x6c, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, + 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x52, 0x0a, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, + 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x63, 0x6f, 0x72, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x50, + 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, + 0x0f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x22, 0x91, 0x01, 0x0a, 0x1d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x50, 0x65, 0x6e, 0x64, 0x69, + 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x72, + 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x73, 0x12, 0x47, 0x0a, 0x0d, 0x61, + 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, - 0x74, 0x65, 0x78, 0x74, 0x22, 0xe7, 0x01, 0x0a, 0x1c, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x50, - 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, - 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x52, 0x0a, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, 0x72, 0x72, - 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, - 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x50, 0x0a, 0x10, - 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, - 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x91, - 0x01, 0x0a, 0x1d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x73, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, + 0x74, 0x65, 0x78, 0x74, 0x22, 0xc8, 0x01, 0x0a, 0x23, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x50, + 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, 0x72, + 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x50, 0x0a, + 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, + 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, + 0x98, 0x01, 0x0a, 0x24, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, + 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x72, 0x72, + 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x73, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x78, 0x74, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, + 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x35, 0x0a, 0x14, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, + 0x73, 0x22, 0x7f, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x73, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, - 0x78, 0x74, 0x22, 0xc8, 0x01, 0x0a, 0x23, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x50, 0x65, 0x6e, - 0x64, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, 0x72, 0x72, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x50, 0x0a, 0x10, 0x63, - 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x98, 0x01, - 0x0a, 0x24, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x73, 0x12, - 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, - 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, - 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, - 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x35, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x73, 0x22, - 0x7f, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x73, 0x12, 0x47, 0x0a, 0x0d, 0x61, 0x75, 0x64, 0x69, 0x74, - 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, - 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, - 0x78, 0x74, 0x52, 0x0c, 0x61, 0x75, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, - 0x22, 0x8d, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x65, 0x61, 0x72, - 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, - 0x6c, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, - 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x22, 0x70, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x46, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, - 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x48, 0x00, 0x52, 0x09, 0x68, - 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x22, 0x53, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, - 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x13, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, - 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2d, 0x61, - 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x73, 0x3b, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x78, 0x74, 0x22, 0x8d, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x65, + 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, + 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x69, + 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x22, 0x70, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x46, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x48, 0x00, 0x52, + 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x22, 0x53, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, + 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, 0x65, 0x76, + 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2d, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x73, 0x3b, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/resource-aggregate/commands/resouces.go b/resource-aggregate/commands/resouces.go index 653881f3e..a82ec198b 100644 --- a/resource-aggregate/commands/resouces.go +++ b/resource-aggregate/commands/resouces.go @@ -1,5 +1,28 @@ package commands +func (c *Content) Clone() *Content { + if c == nil { + return nil + } + + return &Content{ + Data: c.GetData(), + ContentType: c.GetContentType(), + CoapContentFormat: c.GetCoapContentFormat(), + } +} + +func (r *ResourceId) Clone() *ResourceId { + if r == nil { + return nil + } + + return &ResourceId{ + DeviceId: r.GetDeviceId(), + Href: r.GetHref(), + } +} + func (e *EndpointInformation) Clone() *EndpointInformation { if e == nil { return nil diff --git a/resource-aggregate/commands/utils.go b/resource-aggregate/commands/utils.go index b0b973438..be1c39ca7 100644 --- a/resource-aggregate/commands/utils.go +++ b/resource-aggregate/commands/utils.go @@ -1,12 +1,17 @@ package commands import ( + "errors" + "fmt" "net/http" "strings" "github.com/google/uuid" "github.com/plgd-dev/device/v2/schema" + "github.com/plgd-dev/go-coap/v3/message" extCodes "github.com/plgd-dev/hub/v2/grpc-gateway/pb/codes" + "github.com/plgd-dev/kit/v2/codec/cbor" + "github.com/plgd-dev/kit/v2/codec/json" "google.golang.org/grpc/codes" ) @@ -172,3 +177,34 @@ func ResourceIdFromString(v string) *ResourceId { Href: "/" + deviceIDHref[1], } } + +func DecodeContent(content *Content, v interface{}) error { + if content == nil { + return errors.New("cannot parse empty content") + } + + var decode func([]byte, interface{}) error + + switch content.GetContentType() { + case message.AppCBOR.String(), message.AppOcfCbor.String(): + decode = cbor.Decode + case message.AppJSON.String(): + decode = json.Decode + case message.TextPlain.String(): + switch out := v.(type) { + case *string: + *out = string(content.GetData()) + case *[]byte: + *out = content.GetData() + case *interface{}: + *out = string(content.GetData()) + default: + return fmt.Errorf("cannot decode content: invalid type (%T)", v) + } + return nil + default: + return fmt.Errorf("unsupported content type: %v", content.GetContentType()) + } + + return decode(content.GetData(), v) +} diff --git a/resource-aggregate/commands/utils_test.go b/resource-aggregate/commands/utils_test.go index e15f2fa1a..79ab1adf9 100644 --- a/resource-aggregate/commands/utils_test.go +++ b/resource-aggregate/commands/utils_test.go @@ -1,8 +1,12 @@ -package commands +package commands_test import ( "testing" + "github.com/plgd-dev/device/v2/pkg/codec/json" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/test" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) @@ -14,7 +18,7 @@ func TestResourceIdFromString(t *testing.T) { tests := []struct { name string args args - want *ResourceId + want *commands.ResourceId }{ { name: "Nil", @@ -28,7 +32,7 @@ func TestResourceIdFromString(t *testing.T) { args: args{ v: "deviceId/href", }, - want: &ResourceId{ + want: &commands.ResourceId{ DeviceId: "deviceId", Href: "/href", }, @@ -38,7 +42,7 @@ func TestResourceIdFromString(t *testing.T) { args: args{ v: "deviceId/", }, - want: &ResourceId{ + want: &commands.ResourceId{ DeviceId: "deviceId", Href: "/", }, @@ -48,7 +52,7 @@ func TestResourceIdFromString(t *testing.T) { args: args{ v: "//hrefId", }, - want: &ResourceId{ + want: &commands.ResourceId{ DeviceId: "", Href: "/hrefId", }, @@ -56,8 +60,100 @@ func TestResourceIdFromString(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := ResourceIdFromString(tt.args.v) + got := commands.ResourceIdFromString(tt.args.v) require.True(t, proto.Equal(tt.want, got)) }) } } + +func TestDecodeContent(t *testing.T) { + type args struct { + content *commands.Content + } + tests := []struct { + name string + args args + want interface{} + wantErr bool + }{ + { + name: "nil", + args: args{ + content: nil, + }, + wantErr: true, + }, + { + name: "unsupported type", + args: args{ + content: &commands.Content{ + ContentType: "unsupported", + Data: []byte("test"), + }, + }, + wantErr: true, + }, + { + name: "cbor", + args: args{ + content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, map[string]interface{}{"test": "test"}), + }, + }, + want: map[string]interface{}{"test": "test"}, + }, + { + name: "json", + args: args{ + content: &commands.Content{ + ContentType: message.AppJSON.String(), + Data: func() []byte { + d, err := json.Encode(map[string]interface{}{"test": "test"}) + require.NoError(t, err) + return d + }(), + }, + }, + want: map[string]interface{}{"test": "test"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got map[string]interface{} + err := commands.DecodeContent(tt.args.content, &got) + if tt.wantErr { + require.Error(t, err) + return + } + require.Equal(t, tt.want, got) + }) + } +} + +func TestDecodeTextContent(t *testing.T) { + content := &commands.Content{ + ContentType: message.TextPlain.String(), + Data: []byte("test"), + } + var gotStr string + err := commands.DecodeContent(content, &gotStr) + require.NoError(t, err) + require.Equal(t, "test", gotStr) + + var gotBytes []byte + err = commands.DecodeContent(content, &gotBytes) + require.NoError(t, err) + require.Equal(t, []byte("test"), gotBytes) + + var got interface{} + err = commands.DecodeContent(content, &got) + require.NoError(t, err) + str, ok := got.(string) + require.True(t, ok) + require.Equal(t, "test", str) + + var gotMap map[string]interface{} + err = commands.DecodeContent(content, &gotMap) + require.Error(t, err) +} diff --git a/resource-aggregate/cqrs/eventstore/cqldb/load.go b/resource-aggregate/cqrs/eventstore/cqldb/load.go index 3e14ae108..b7d1574a5 100644 --- a/resource-aggregate/cqrs/eventstore/cqldb/load.go +++ b/resource-aggregate/cqrs/eventstore/cqldb/load.go @@ -143,7 +143,7 @@ func snapshotQueriesToFilter(deviceID string, queries []eventstore.SnapshotQuery } aggrs := make([]string, 0, len(queries)) for _, q := range queries { - if q.AggregateID != "" { + if q.AggregateID != "" && q.AggregateID != uuid.Nil.String() { aggrs = append(aggrs, q.AggregateID) } } @@ -192,7 +192,7 @@ func (s *EventStore) loadFromSnapshotByGroup(ctx context.Context, groupID string func normalizeQueries(queries []eventstore.SnapshotQuery) map[string][]eventstore.SnapshotQuery { normalizeQuery := make(map[string][]eventstore.SnapshotQuery, len(queries)) for _, query := range queries { - if query.AggregateID == "" { + if query.AggregateID == "" || query.AggregateID == uuid.Nil.String() { normalizeQuery[query.GroupID] = nil continue } diff --git a/resource-aggregate/cqrs/eventstore/mongodb/load.go b/resource-aggregate/cqrs/eventstore/mongodb/load.go index 3826c16b2..b449ad334 100644 --- a/resource-aggregate/cqrs/eventstore/mongodb/load.go +++ b/resource-aggregate/cqrs/eventstore/mongodb/load.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/google/uuid" "github.com/hashicorp/go-multierror" pkgTime "github.com/plgd-dev/hub/v2/pkg/time" "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventstore" @@ -177,7 +178,7 @@ func (r *queryResolver) set(query eventstore.VersionQuery) error { if query.GroupID == "" { return fmt.Errorf("invalid GroupID('%v')", query.GroupID) } - if query.AggregateID == "" { + if query.AggregateID == "" || query.AggregateID == uuid.Nil.String() { return fmt.Errorf("invalid AggregateID('%v')", query.AggregateID) } r.aggregateVersions[query.AggregateID] = query.Version @@ -337,7 +338,7 @@ func snapshotQueriesToMongoQuery(groupID string, queries []eventstore.SnapshotQu resourceQueries := make([]bson.D, 0, 32) typeQueries := make([]bson.D, 0, 32) for _, q := range queries { - if q.AggregateID == "" && len(q.Types) == 0 { + if (q.AggregateID == "" || q.AggregateID == uuid.Nil.String()) && len(q.Types) == 0 { opts.SetHint(groupIDQueryIndex) return []mongoQuery{ { @@ -348,13 +349,13 @@ func snapshotQueriesToMongoQuery(groupID string, queries []eventstore.SnapshotQu }, } } - if q.AggregateID == "" && len(q.Types) > 0 { + if (q.AggregateID == "" || q.AggregateID == uuid.Nil.String()) && len(q.Types) > 0 { optsTypes.SetHint(groupIDTypesQueryIndex) typeQueries = append(typeQueries, bson.D{{Key: groupIDKey, Value: groupID}, {Key: typesKey, Value: bson.M{"$all": q.Types}}, {Key: isActiveKey, Value: true}}) continue } - if q.AggregateID != "" { + if q.AggregateID != "" && q.AggregateID != uuid.Nil.String() { opts.SetHint(groupIDaggregateIDQueryIndex) resourceQueries = append(resourceQueries, bson.D{{Key: groupIDKey, Value: groupID}, {Key: aggregateIDKey, Value: q.AggregateID}, {Key: isActiveKey, Value: true}}) } @@ -422,7 +423,7 @@ func uniqueQueryWithEmptyAggregateID(queries []eventstore.SnapshotQuery, query e func uniqueQueryHandleAtIndex(queries []eventstore.SnapshotQuery, query eventstore.SnapshotQuery, idx int) ([]eventstore.SnapshotQuery, bool) { q := queries[idx] - if q.AggregateID == "" && len(q.Types) == 0 || resourceTypesIsSubset(q.Types, query.Types) { + if (q.AggregateID == "" || q.AggregateID == uuid.Nil.String()) && len(q.Types) == 0 || resourceTypesIsSubset(q.Types, query.Types) { return queries, true // No need to add more specific one if there's a general query } @@ -446,7 +447,7 @@ func uniqueQueryHandleAtIndex(queries []eventstore.SnapshotQuery, query eventsto } func uniqueQuery(queries []eventstore.SnapshotQuery, query eventstore.SnapshotQuery) []eventstore.SnapshotQuery { - if query.AggregateID == "" { + if query.AggregateID == "" || query.AggregateID == uuid.Nil.String() { return uniqueQueryWithEmptyAggregateID(queries, query) } diff --git a/resource-aggregate/cqrs/projection/projection.go b/resource-aggregate/cqrs/projection/projection.go index 76b600f58..8655cead1 100644 --- a/resource-aggregate/cqrs/projection/projection.go +++ b/resource-aggregate/cqrs/projection/projection.go @@ -123,6 +123,14 @@ func (p *Projection) Models(onModel func(eventstore.Model) (wantNext bool), reso p.cqrsProjection.Models(q, onModel) } +func (p *Projection) GroupsModels(onModel func(eventstore.Model) (wantNext bool), groups ...string) { + q := make([]eventstore.SnapshotQuery, 0, len(groups)) + for _, group := range groups { + q = append(q, eventstore.SnapshotQuery{GroupID: group}) + } + p.cqrsProjection.Models(q, onModel) +} + // ForceUpdate invokes update registered resource model from evenstore. func (p *Projection) ForceUpdate(ctx context.Context, resourceID *commands.ResourceId) error { v, ok := p.refCountMap.LoadWithFunc(resourceID.GetDeviceId(), func(v interface{}) interface{} { diff --git a/resource-aggregate/events/resourceStateSnapshotTaken.go b/resource-aggregate/events/resourceStateSnapshotTaken.go index 1fb060e94..8697e4ba2 100644 --- a/resource-aggregate/events/resourceStateSnapshotTaken.go +++ b/resource-aggregate/events/resourceStateSnapshotTaken.go @@ -22,8 +22,9 @@ import ( const eventTypeResourceStateSnapshotTaken = "resourcestatesnapshottaken" const ( - errInvalidVersion = "invalid version for events" - errInvalidCommandMetadata = "invalid command metadata" + errInvalidVersion = "invalid version for events" + errResourceChangedNotExists = "resource changed not exists" + errInvalidCommandMetadata = "invalid command metadata" ) func (e *ResourceStateSnapshotTaken) AggregateID() string { @@ -111,7 +112,7 @@ func (e *ResourceStateSnapshotTaken) processValidUntil(v resourceValidUntilValid return true } -func (e *ResourceStateSnapshotTaken) checkForDuplicitCorrelationID(correlationID string, now time.Time) error { +func (e *ResourceStateSnapshotTaken) checkForDuplicityCorrelationID(correlationID string, now time.Time) error { for _, event := range e.GetResourceCreatePendings() { if event.IsExpired(now) { continue @@ -152,7 +153,7 @@ func (e *ResourceStateSnapshotTaken) handleEventResourceCreatePending(createPend if ok := e.processValidUntil(createPending, now); !ok { return nil } - err := e.checkForDuplicitCorrelationID(createPending.GetAuditContext().GetCorrelationId(), now) + err := e.checkForDuplicityCorrelationID(createPending.GetAuditContext().GetCorrelationId(), now) if err != nil { return err } @@ -176,7 +177,7 @@ func (e *ResourceStateSnapshotTaken) handleEventResourceUpdatePending(updatePend if ok := e.processValidUntil(updatePending, now); !ok { return nil } - err := e.checkForDuplicitCorrelationID(updatePending.GetAuditContext().GetCorrelationId(), now) + err := e.checkForDuplicityCorrelationID(updatePending.GetAuditContext().GetCorrelationId(), now) if err != nil { return err } @@ -193,7 +194,7 @@ func (e *ResourceStateSnapshotTaken) handleEventResourceRetrievePending(retrieve if ok := e.processValidUntil(retrievePending, now); !ok { return nil } - err := e.checkForDuplicitCorrelationID(retrievePending.GetAuditContext().GetCorrelationId(), now) + err := e.checkForDuplicityCorrelationID(retrievePending.GetAuditContext().GetCorrelationId(), now) if err != nil { return err } @@ -210,7 +211,7 @@ func (e *ResourceStateSnapshotTaken) handleEventResourceDeletePending(deletePend if ok := e.processValidUntil(deletePending, now); !ok { return nil } - err := e.checkForDuplicitCorrelationID(deletePending.GetAuditContext().GetCorrelationId(), now) + err := e.checkForDuplicityCorrelationID(deletePending.GetAuditContext().GetCorrelationId(), now) if err != nil { return err } @@ -227,14 +228,8 @@ func RemoveIndex(s []int, index int) []int { } func (e *ResourceStateSnapshotTaken) handleEventResourceCreated(created *ResourceCreated) error { - index := -1 resourceCreatePendings := e.GetResourceCreatePendings() - for i, event := range resourceCreatePendings { - if event.GetAuditContext().GetCorrelationId() == created.GetAuditContext().GetCorrelationId() { - index = i - break - } - } + index := findResourceOperationPendingIndex(created.GetAuditContext().GetCorrelationId(), resourceCreatePendings) if index < 0 { return status.Errorf(codes.InvalidArgument, "cannot find resource create pending event with correlationId('%v')", created.GetAuditContext().GetCorrelationId()) } @@ -247,15 +242,18 @@ func (e *ResourceStateSnapshotTaken) handleEventResourceCreated(created *Resourc return nil } -func (e *ResourceStateSnapshotTaken) handleEventResourceUpdated(updated *ResourceUpdated) error { - index := -1 - resourceUpdatePendings := e.GetResourceUpdatePendings() - for i, event := range resourceUpdatePendings { - if event.GetAuditContext().GetCorrelationId() == updated.GetAuditContext().GetCorrelationId() { - index = i - break +func findResourceOperationPendingIndex[Op interface{ GetAuditContext() *commands.AuditContext }](correlationID string, ops []Op) int { + for i, event := range ops { + if event.GetAuditContext().GetCorrelationId() == correlationID { + return i } } + return -1 +} + +func (e *ResourceStateSnapshotTaken) handleEventResourceUpdated(updated *ResourceUpdated) error { + resourceUpdatePendings := e.GetResourceUpdatePendings() + index := findResourceOperationPendingIndex(updated.GetAuditContext().GetCorrelationId(), resourceUpdatePendings) if index < 0 { return status.Errorf(codes.InvalidArgument, "cannot find resource update pending event with correlationId('%v')", updated.GetAuditContext().GetCorrelationId()) } @@ -269,14 +267,8 @@ func (e *ResourceStateSnapshotTaken) handleEventResourceUpdated(updated *Resourc } func (e *ResourceStateSnapshotTaken) handleEventResourceRetrieved(retrieved *ResourceRetrieved) error { - index := -1 resourceRetrievePendings := e.GetResourceRetrievePendings() - for i, event := range resourceRetrievePendings { - if event.GetAuditContext().GetCorrelationId() == retrieved.GetAuditContext().GetCorrelationId() { - index = i - break - } - } + index := findResourceOperationPendingIndex(retrieved.GetAuditContext().GetCorrelationId(), resourceRetrievePendings) if index < 0 { return status.Errorf(codes.InvalidArgument, "cannot find resource retrieve pending event with correlationId('%v')", retrieved.GetAuditContext().GetCorrelationId()) } @@ -312,26 +304,27 @@ func (e *ResourceStateSnapshotTaken) handleEventResourceChanged(changed *Resourc return false } +func (e *ResourceStateSnapshotTaken) findResourceDeletePendingIndex(status commands.Status, correlationID string) (bool, int) { + if status == commands.Status_OK || status == commands.Status_ACCEPTED { + return true, -1 + } + return false, findResourceOperationPendingIndex(correlationID, e.GetResourceDeletePendings()) +} + func (e *ResourceStateSnapshotTaken) handleEventResourceDeleted(deleted *ResourceDeleted) error { - if deleted.GetStatus() == commands.Status_OK || deleted.GetStatus() == commands.Status_ACCEPTED { + deleteResource, deletePendingIndex := e.findResourceDeletePendingIndex(deleted.GetStatus(), deleted.GetAuditContext().GetCorrelationId()) + switch { + case deleteResource: e.ResourceCreatePendings = nil e.ResourceRetrievePendings = nil e.ResourceDeletePendings = nil e.ResourceUpdatePendings = nil - } else { - index := -1 + case deletePendingIndex >= 0: resourceDeletePendings := e.GetResourceDeletePendings() - for i, event := range resourceDeletePendings { - if event.GetAuditContext().GetCorrelationId() == deleted.GetAuditContext().GetCorrelationId() { - index = i - break - } - } - if index < 0 { - return status.Errorf(codes.InvalidArgument, "cannot find resource delete pending event with correlationId('%v')", deleted.GetAuditContext().GetCorrelationId()) - } - resourceDeletePendings = append(resourceDeletePendings[:index], resourceDeletePendings[index+1:]...) + resourceDeletePendings = append(resourceDeletePendings[:deletePendingIndex], resourceDeletePendings[deletePendingIndex+1:]...) e.ResourceDeletePendings = resourceDeletePendings + default: + return status.Errorf(codes.InvalidArgument, "cannot find resource delete pending event with correlationId('%v')", deleted.GetAuditContext().GetCorrelationId()) } e.ResourceId = deleted.GetResourceId() e.EventMetadata = deleted.GetEventMetadata() @@ -823,10 +816,66 @@ func (e *ResourceStateSnapshotTakenForCommand) handleCreateResourceRequest(ctx c return []eventstore.Event{&rc}, nil } +func (e *ResourceStateSnapshotTakenForCommand) validateCancelPendingCommandsForNotExistingResource(req *commands.CancelPendingCommandsRequest) bool { + if len(e.GetResourceUpdatePendings()) == 0 && len(e.GetResourceCreatePendings()) == 0 && len(e.GetResourceDeletePendings()) == 0 { + return false + } + if len(req.GetCorrelationIdFilter()) == 0 { + return true + } + correlationIdFilter := strings.MakeSet(req.GetCorrelationIdFilter()...) + for _, event := range e.GetResourceUpdatePendings() { + if correlationIdFilter.HasOneOf(event.GetAuditContext().GetCorrelationId()) { + return true + } + } + for _, event := range e.GetResourceCreatePendings() { + if correlationIdFilter.HasOneOf(event.GetAuditContext().GetCorrelationId()) { + return true + } + } + for _, event := range e.GetResourceDeletePendings() { + if correlationIdFilter.HasOneOf(event.GetAuditContext().GetCorrelationId()) { + return true + } + } + return false +} + +func (e *ResourceStateSnapshotTakenForCommand) validateCommandForNotExistingResource(cmd aggregate.Command) bool { + if e.GetLatestResourceChange() != nil { + // resource exists + return true + } + switch req := cmd.(type) { + case *commands.NotifyResourceChangedRequest: + // NotifyResourceChangedRequest can have any version + return true + case *commands.UpdateResourceRequest: + // UpdateResourceRequest can have version 0 when if not exists is set + return req.GetForce() + case *commands.ConfirmResourceUpdateRequest: + return findResourceOperationPendingIndex(req.GetCorrelationId(), e.GetResourceUpdatePendings()) >= 0 + case *commands.CreateResourceRequest: + // CreateResourceRequest can have version 0 when if not exists is set + return req.GetForce() + case *commands.ConfirmResourceCreateRequest: + return findResourceOperationPendingIndex(req.GetCorrelationId(), e.GetResourceCreatePendings()) >= 0 + case *commands.DeleteResourceRequest: + // DeleteResourceRequest can have version 0 when if not exists is set + return req.GetForce() + case *commands.ConfirmResourceDeleteRequest: + deleteResource, deletePending := e.findResourceDeletePendingIndex(req.GetStatus(), req.GetCorrelationId()) + return deleteResource || deletePending >= 0 + case *commands.CancelPendingCommandsRequest: + return e.validateCancelPendingCommandsForNotExistingResource(req) + } + return false +} + func (e *ResourceStateSnapshotTakenForCommand) HandleCommand(ctx context.Context, cmd aggregate.Command, newVersion uint64) ([]eventstore.Event, error) { - // only NotifyResourceChangedRequest can have version 0 - if _, ok := cmd.(*commands.NotifyResourceChangedRequest); !ok && newVersion == 0 { - return nil, status.Errorf(codes.NotFound, errInvalidVersion) + if !e.validateCommandForNotExistingResource(cmd) { + return nil, status.Errorf(codes.NotFound, errResourceChangedNotExists) } switch req := cmd.(type) { diff --git a/resource-aggregate/events/resourceStateSnapshotTaken_internal_test.go b/resource-aggregate/events/resourceStateSnapshotTaken_internal_test.go new file mode 100644 index 000000000..6ebb1682c --- /dev/null +++ b/resource-aggregate/events/resourceStateSnapshotTaken_internal_test.go @@ -0,0 +1,49 @@ +package events + +import ( + "testing" + + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/stretchr/testify/assert" +) + +func TestResourceStateSnapshotTakenForCommand_ValidateCancelPendingCommandsForNotExistingResource(t *testing.T) { + e := NewResourceStateSnapshotTakenForCommand("", "", "", nil) + + // Test when there are no pending commands + assert.False(t, e.validateCancelPendingCommandsForNotExistingResource(&commands.CancelPendingCommandsRequest{})) + + // Test when there are pending commands but no correlation ID filter + e.ResourceUpdatePendings = []*ResourceUpdatePending{ + { + AuditContext: &commands.AuditContext{ + CorrelationId: "1", + }, + }, + } + assert.True(t, e.validateCancelPendingCommandsForNotExistingResource(&commands.CancelPendingCommandsRequest{})) + + // Test when there are pending commands and a correlation ID filter + e.ResourceCreatePendings = []*ResourceCreatePending{ + { + AuditContext: &commands.AuditContext{ + CorrelationId: "2", + }, + }, + } + e.ResourceDeletePendings = []*ResourceDeletePending{ + { + AuditContext: &commands.AuditContext{ + CorrelationId: "3", + }, + }, + } + assert.True(t, e.validateCancelPendingCommandsForNotExistingResource(&commands.CancelPendingCommandsRequest{ + CorrelationIdFilter: []string{"2", "4"}, + })) + + // Test when there are no pending commands matching the correlation ID filter + assert.False(t, e.validateCancelPendingCommandsForNotExistingResource(&commands.CancelPendingCommandsRequest{ + CorrelationIdFilter: []string{"5", "6"}, + })) +} diff --git a/resource-aggregate/events/resourceStateSnapshotTaken_test.go b/resource-aggregate/events/resourceStateSnapshotTaken_test.go index 18af0389c..fa9df8d7a 100644 --- a/resource-aggregate/events/resourceStateSnapshotTaken_test.go +++ b/resource-aggregate/events/resourceStateSnapshotTaken_test.go @@ -6,10 +6,13 @@ import ( "time" "github.com/plgd-dev/go-coap/v3/message" + grpcgwPb "github.com/plgd-dev/hub/v2/grpc-gateway/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/aggregate" "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventstore" "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventstore/test" "github.com/plgd-dev/hub/v2/resource-aggregate/events" + "github.com/plgd-dev/hub/v2/test/pb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" @@ -72,6 +75,553 @@ func TestResourceStateSnapshotTakenResourceTypes(t *testing.T) { require.Equal(t, resourceTypes, e.Types()) } +func TestResourceStateSnapshotHandleCommand(t *testing.T) { + deviceID := "deviceId" + correlationID := "correlationID" + connectionID := "connectionID" + userID := "userID" + hubID := "hubID" + resourceHref := "/a" + resourceTypes := []string{"type1", "type2"} + + type cmd struct { + cmd aggregate.Command + newVersion uint64 + wantErr bool + want []*grpcgwPb.Event + } + tests := []struct { + name string + cmds []cmd + }{ + { + name: "notify-resource-changed", + cmds: []cmd{ + { + cmd: &commands.NotifyResourceChangedRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + Status: commands.Status_OK, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 0, + }, + ResourceTypes: resourceTypes, + }, + newVersion: 0, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceChanged{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, "", userID), + OpenTelemetryCarrier: map[string]string{}, + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + Status: commands.Status_OK, + EventMetadata: &events.EventMetadata{ + Version: 0, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 0, + }, + ResourceTypes: resourceTypes, + }), + }, + }, + }, + }, + { + name: "update-resource-not-exists", + cmds: []cmd{ + { + cmd: &commands.UpdateResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 1, + }, + }, + wantErr: true, + }, + }, + }, + { + name: "update-resource-not-exists-with-flag", + cmds: []cmd{ + { + cmd: &commands.UpdateResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 1, + }, + Force: true, + CorrelationId: correlationID, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceUpdatePending{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 0, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 1, + }, + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + }), + }, + }, + { + cmd: &commands.ConfirmResourceUpdateRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 2, + }, + CorrelationId: correlationID, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceUpdated{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 1, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 2, + }, + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + }), + }, + }, + }, + }, + { + name: "update-resource-not-exists-with-flag-and-cancel", + cmds: []cmd{ + { + cmd: &commands.UpdateResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 1, + }, + Force: true, + CorrelationId: correlationID, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceUpdatePending{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 0, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 1, + }, + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + }), + }, + }, + { + cmd: &commands.CancelPendingCommandsRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + CorrelationIdFilter: []string{correlationID}, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 2, + }, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceUpdated{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 1, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 2, + }, + Status: commands.Status_CANCELED, + }), + }, + }, + }, + }, + { + name: "create-resource-not-exists", + cmds: []cmd{ + { + cmd: &commands.CreateResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 1, + }, + }, + wantErr: true, + }, + }, + }, + { + name: "create-resource-not-exists-with-flag", + cmds: []cmd{ + { + cmd: &commands.CreateResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 1, + }, + Force: true, + CorrelationId: correlationID, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceCreatePending{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 0, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 1, + }, + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + }), + }, + }, + { + cmd: &commands.ConfirmResourceCreateRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 2, + }, + CorrelationId: correlationID, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceCreated{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 1, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 2, + }, + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + }), + }, + }, + }, + }, + { + name: "create-resource-not-exists-with-flag-and-cancel", + cmds: []cmd{ + { + cmd: &commands.CreateResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 1, + }, + Force: true, + CorrelationId: correlationID, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceCreatePending{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 0, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 1, + }, + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + CoapContentFormat: int32(message.AppJSON), + }, + }), + }, + }, + { + cmd: &commands.CancelPendingCommandsRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + CorrelationIdFilter: []string{correlationID}, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 2, + }, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceCreated{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 1, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 2, + }, + Status: commands.Status_CANCELED, + }), + }, + }, + }, + }, + { + name: "delete-resource-not-exists", + cmds: []cmd{ + { + cmd: &commands.DeleteResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 1, + }, + }, + wantErr: true, + }, + }, + }, + { + name: "delete-resource-not-exists-with-flag", + cmds: []cmd{ + { + cmd: &commands.DeleteResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 1, + }, + Force: true, + CorrelationId: correlationID, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceDeletePending{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 0, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 1, + }, + }), + }, + }, + { + cmd: &commands.ConfirmResourceDeleteRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 2, + }, + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + }, + CorrelationId: correlationID, + Status: commands.Status_METHOD_NOT_ALLOWED, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceDeleted{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 1, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 2, + }, + Content: &commands.Content{ + Data: []byte("{}"), + ContentType: "application/json", + }, + Status: commands.Status_METHOD_NOT_ALLOWED, + }), + }, + }, + }, + }, + { + name: "delete-resource-not-exists-with-flag-and-cancel", + cmds: []cmd{ + { + cmd: &commands.DeleteResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 1, + }, + Force: true, + CorrelationId: correlationID, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceDeletePending{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 0, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 1, + }, + }), + }, + }, + { + cmd: &commands.CancelPendingCommandsRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 2, + }, + CorrelationIdFilter: []string{correlationID}, + }, + want: []*grpcgwPb.Event{ + pb.ToEvent(&events.ResourceDeleted{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + AuditContext: commands.NewAuditContext(userID, correlationID, userID), + OpenTelemetryCarrier: map[string]string{}, + EventMetadata: &events.EventMetadata{ + Version: 1, + Timestamp: 0, + ConnectionId: connectionID, + Sequence: 2, + }, + Status: commands.Status_CANCELED, + }), + }, + }, + }, + }, + { + name: "retrieve-resource-not-exists", + cmds: []cmd{ + { + cmd: &commands.RetrieveResourceRequest{ + ResourceId: commands.NewResourceID(deviceID, resourceHref), + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: connectionID, + Sequence: 1, + }, + }, + wantErr: true, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := events.NewResourceStateSnapshotTakenForCommand(userID, userID, hubID, nil) + for idx, cmd := range tt.cmds { + res, err := e.HandleCommand(context.TODO(), cmd.cmd, cmd.newVersion) + if cmd.wantErr { + require.Error(t, err, "cmd: %v", idx) + } else { + require.NoError(t, err, "cmd: %v", idx) + var got []*grpcgwPb.Event + if len(res) > 0 { + got = make([]*grpcgwPb.Event, 0, len(res)) + for _, e := range res { + grpcEv := pb.ToEvent(e) + d1, err := proto.Marshal(grpcEv) + require.NoError(t, err) + var v grpcgwPb.Event + err = proto.Unmarshal(d1, &v) + require.NoError(t, err) + got = append(got, &v) + } + } + pb.CmpEvents(t, cmd.want, got) + } + } + }) + } +} + func TestResourceStateSnapshotTakenHandle(t *testing.T) { resourceTypes := []string{"type1", "type2"} type args struct { diff --git a/resource-aggregate/events/resourceUpdated.go b/resource-aggregate/events/resourceUpdated.go index 6c7bac5c8..52871bf6d 100644 --- a/resource-aggregate/events/resourceUpdated.go +++ b/resource-aggregate/events/resourceUpdated.go @@ -1,6 +1,7 @@ package events import ( + "slices" "time" pkgTime "github.com/plgd-dev/hub/v2/pkg/time" @@ -65,6 +66,21 @@ func (e *ResourceUpdated) CopyData(event *ResourceUpdated) { e.ResourceTypes = event.GetResourceTypes() } +func (e *ResourceUpdated) Clone() *ResourceUpdated { + if e == nil { + return nil + } + return &ResourceUpdated{ + ResourceId: e.GetResourceId().Clone(), + Status: e.GetStatus(), + Content: e.GetContent().Clone(), + AuditContext: e.GetAuditContext().Clone(), + EventMetadata: e.GetEventMetadata().Clone(), + OpenTelemetryCarrier: e.GetOpenTelemetryCarrier(), + ResourceTypes: slices.Clone(e.GetResourceTypes()), + } +} + func (e *ResourceUpdated) CheckInitialized() bool { return e.GetResourceId() != nil && e.GetStatus() != commands.Status(0) && diff --git a/resource-aggregate/pb/commands.proto b/resource-aggregate/pb/commands.proto index 41bf7b09e..a4d463001 100644 --- a/resource-aggregate/pb/commands.proto +++ b/resource-aggregate/pb/commands.proto @@ -166,6 +166,7 @@ message UpdateResourceRequest { string resource_interface = 4; int64 time_to_live = 5; // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). CommandMetadata command_metadata = 6; + bool force = 7; // if true, the command will be executed even if the resource does not exist } message UpdateResourceResponse { @@ -306,6 +307,7 @@ message DeleteResourceRequest { int64 time_to_live = 3; // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). CommandMetadata command_metadata = 4; string resource_interface = 5; + bool force = 6; // if true, the command will be executed even if the resource does not exist } message DeleteResourceResponse { @@ -379,6 +381,7 @@ message CreateResourceRequest { Content content = 3; int64 time_to_live = 4; // command validity in nanoseconds. 0 means forever and minimal value is 100000000 (100ms). CommandMetadata command_metadata = 5; + bool force = 6; // if true, the command will be executed even if the resource does not exist } message CreateResourceResponse { diff --git a/resource-aggregate/service/aggregate.go b/resource-aggregate/service/aggregate.go index f25bdad35..a7d73b5d0 100644 --- a/resource-aggregate/service/aggregate.go +++ b/resource-aggregate/service/aggregate.go @@ -21,16 +21,41 @@ type Aggregate struct { eventstore eventstore.EventStore } -func NewResourceStateFactoryModel(userID, owner, hubID string) func(context.Context, string, string) (cqrsAggregate.AggregateModel, error) { +type resourceStateModel struct { + resourceState *events.ResourceStateSnapshotTakenForCommand + resourceLinks *events.ResourceLinksSnapshotTakenForCommand +} + +func newResourceStateModel(userID, owner, hubID string) *resourceStateModel { resourceLinks := events.NewResourceLinksSnapshotTakenForCommand(userID, owner, hubID) resourceState := events.NewResourceStateSnapshotTakenForCommand(userID, owner, hubID, resourceLinks) - return func(_ context.Context, groupID string, aggregateID string) (cqrsAggregate.AggregateModel, error) { - resID := commands.NewResourceID(groupID, commands.ResourceLinksHref) - if aggregateID == resID.ToUUID().String() { - return resourceLinks, nil - } - return resourceState, nil + return &resourceStateModel{ + resourceState: resourceState, + resourceLinks: resourceLinks, + } +} + +func (r *resourceStateModel) isPublished(resourceID *commands.ResourceId) bool { + if r.resourceLinks == nil { + return false } + if r.resourceLinks.GetResources() == nil { + return false + } + return r.resourceLinks.GetResources()[resourceID.GetHref()] != nil +} + +func (r *resourceStateModel) model(_ context.Context, groupID string, aggregateID string) (cqrsAggregate.AggregateModel, error) { + resID := commands.NewResourceID(groupID, commands.ResourceLinksHref) + if aggregateID == resID.ToUUID().String() { + return r.resourceLinks, nil + } + return r.resourceState, nil +} + +func NewResourceStateFactoryModel(userID, owner, hubID string) func(context.Context, string, string) (cqrsAggregate.AggregateModel, error) { + m := newResourceStateModel(userID, owner, hubID) + return m.model } func NewResourceLinksFactoryModel(userID, owner, hubID string) func(context.Context, string, string) (cqrsAggregate.AggregateModel, error) { diff --git a/resource-aggregate/service/grpcApi.go b/resource-aggregate/service/grpcApi.go index fcecaf4ff..91c829b13 100644 --- a/resource-aggregate/service/grpcApi.go +++ b/resource-aggregate/service/grpcApi.go @@ -236,6 +236,16 @@ func (r RequestHandler) NotifyResourceChanged(ctx context.Context, request *comm }, nil } +// check update, delete, create requests when force is set +func wantToPublish(m *resourceStateModel, force bool, resourceID *commands.ResourceId) bool { + if !force { + // we want to publish the source when force is not set + return true + } + // when the resource is not published we do not want to publish event to the eventbus + return m.isPublished(resourceID) +} + func (r RequestHandler) UpdateResource(ctx context.Context, request *commands.UpdateResourceRequest) (*commands.UpdateResourceResponse, error) { userID, owner, err := r.validateAccessToDevice(ctx, request.GetResourceId().GetDeviceId()) if err != nil { @@ -243,7 +253,8 @@ func (r RequestHandler) UpdateResource(ctx context.Context, request *commands.Up } request.TimeToLive = checkTimeToLiveForDefault(r.config.Clients.Eventstore.DefaultCommandTimeToLive, request.GetTimeToLive()) - aggregate, err := NewResourceAggregate(request.GetResourceId(), r.eventstore, NewResourceStateFactoryModel(userID, owner, r.config.HubID), cqrsAggregate.NewDefaultRetryFunc(r.config.Clients.Eventstore.ConcurrencyExceptionMaxRetry), true) + m := newResourceStateModel(userID, owner, r.config.HubID) + aggregate, err := NewResourceAggregate(request.GetResourceId(), r.eventstore, m.model, cqrsAggregate.NewDefaultRetryFunc(r.config.Clients.Eventstore.ConcurrencyExceptionMaxRetry), true) if err != nil { return nil, log.LogAndReturnError(grpc.ForwardErrorf(codes.InvalidArgument, "cannot update resource content: %v", err)) } @@ -253,7 +264,9 @@ func (r RequestHandler) UpdateResource(ctx context.Context, request *commands.Up return nil, log.LogAndReturnError(grpc.ForwardErrorf(codes.Internal, "cannot update resource content: %v", err)) } - PublishEvents(r.publisher, owner, aggregate.DeviceID(), aggregate.ResourceID(), events, r.logger) + if wantToPublish(m, request.GetForce(), request.GetResourceId()) { + PublishEvents(r.publisher, owner, aggregate.DeviceID(), aggregate.ResourceID(), events, r.logger) + } var validUntil int64 for _, e := range events { @@ -356,7 +369,8 @@ func (r RequestHandler) DeleteResource(ctx context.Context, request *commands.De } request.TimeToLive = checkTimeToLiveForDefault(r.config.Clients.Eventstore.DefaultCommandTimeToLive, request.GetTimeToLive()) - aggregate, err := NewResourceAggregate(request.GetResourceId(), r.eventstore, NewResourceStateFactoryModel(userID, owner, r.config.HubID), cqrsAggregate.NewDefaultRetryFunc(r.config.Clients.Eventstore.ConcurrencyExceptionMaxRetry), true) + m := newResourceStateModel(userID, owner, r.config.HubID) + aggregate, err := NewResourceAggregate(request.GetResourceId(), r.eventstore, m.model, cqrsAggregate.NewDefaultRetryFunc(r.config.Clients.Eventstore.ConcurrencyExceptionMaxRetry), true) if err != nil { return nil, log.LogAndReturnError(grpc.ForwardErrorf(codes.InvalidArgument, "cannot delete resource: %v", err)) } @@ -366,7 +380,9 @@ func (r RequestHandler) DeleteResource(ctx context.Context, request *commands.De return nil, log.LogAndReturnError(grpc.ForwardErrorf(codes.Internal, "cannot delete resource: %v", err)) } - PublishEvents(r.publisher, owner, aggregate.DeviceID(), aggregate.ResourceID(), events, r.logger) + if wantToPublish(m, request.GetForce(), request.GetResourceId()) { + PublishEvents(r.publisher, owner, aggregate.DeviceID(), aggregate.ResourceID(), events, r.logger) + } var validUntil int64 for _, e := range events { @@ -414,7 +430,8 @@ func (r RequestHandler) CreateResource(ctx context.Context, request *commands.Cr } request.TimeToLive = checkTimeToLiveForDefault(r.config.Clients.Eventstore.DefaultCommandTimeToLive, request.GetTimeToLive()) - aggregate, err := NewResourceAggregate(request.GetResourceId(), r.eventstore, NewResourceStateFactoryModel(userID, owner, r.config.HubID), cqrsAggregate.NewDefaultRetryFunc(r.config.Clients.Eventstore.ConcurrencyExceptionMaxRetry), true) + m := newResourceStateModel(userID, owner, r.config.HubID) + aggregate, err := NewResourceAggregate(request.GetResourceId(), r.eventstore, m.model, cqrsAggregate.NewDefaultRetryFunc(r.config.Clients.Eventstore.ConcurrencyExceptionMaxRetry), true) if err != nil { return nil, log.LogAndReturnError(grpc.ForwardErrorf(codes.InvalidArgument, "cannot create resource: %v", err)) } @@ -424,7 +441,9 @@ func (r RequestHandler) CreateResource(ctx context.Context, request *commands.Cr return nil, log.LogAndReturnError(grpc.ForwardErrorf(codes.Internal, "cannot create resource: %v", err)) } - PublishEvents(r.publisher, owner, aggregate.DeviceID(), aggregate.ResourceID(), events, r.logger) + if wantToPublish(m, request.GetForce(), request.GetResourceId()) { + PublishEvents(r.publisher, owner, aggregate.DeviceID(), aggregate.ResourceID(), events, r.logger) + } var validUntil int64 for _, e := range events { diff --git a/resource-directory/service/deviceDirectory.go b/resource-directory/service/deviceDirectory.go index 2e44ccffb..8d143eff8 100644 --- a/resource-directory/service/deviceDirectory.go +++ b/resource-directory/service/deviceDirectory.go @@ -139,7 +139,7 @@ func (dd *DeviceDirectory) sendDevices(deviceIDs strings.Set, req *pb.GetDevices return nil } resourceIdFilter := []*commands.ResourceId{commands.NewResourceID(m.GetDeviceID(), device.ResourceURI)} - return dd.projection.LoadResourcesWithLinks(resourceIdFilter, typeFilter, toReloadDevices, func(resource *Resource) error { + return dd.projection.LoadResources(srv.Context(), resourceIdFilter, typeFilter, false, toReloadDevices, func(resource *Resource) error { var device Device err = updateDevice(&device, resource) if err != nil { diff --git a/resource-directory/service/getPendingCommands_test.go b/resource-directory/service/getPendingCommands_test.go index b99d882fd..16228fefe 100644 --- a/resource-directory/service/getPendingCommands_test.go +++ b/resource-directory/service/getPendingCommands_test.go @@ -361,9 +361,7 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { secureGWShutdown() createFn := func(timeToLive time.Duration) { - createCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errC := c.CreateResource(createCtx, &pb.CreateResourceRequest{ + _, errC := c.CreateResource(ctx, &pb.CreateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, device.ResourceURI), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -372,8 +370,9 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { }), }, TimeToLive: int64(timeToLive), + Async: true, }) - require.Error(t, errC) + require.NoError(t, errC) } createFn(time.Millisecond * 500) // for test expired event createFn(0) @@ -391,9 +390,7 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { retrieveFn(0) updateFn := func(timeToLive time.Duration) { - updateCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errU := c.UpdateResource(updateCtx, &pb.UpdateResourceRequest{ + _, errU := c.UpdateResource(ctx, &pb.UpdateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1")), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -402,20 +399,20 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { }), }, TimeToLive: int64(timeToLive), + Async: true, }) - require.Error(t, errU) + require.NoError(t, errU) } updateFn(time.Millisecond * 500) // for test expired event updateFn(0) deleteFn := func(timeToLive time.Duration) { - deleteCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errD := c.DeleteResource(deleteCtx, &pb.DeleteResourceRequest{ + _, errD := c.DeleteResource(ctx, &pb.DeleteResourceRequest{ ResourceId: commands.NewResourceID(deviceID, device.ResourceURI), TimeToLive: int64(timeToLive), + Async: true, }) - require.Error(t, errD) + require.NoError(t, errD) } deleteFn(time.Millisecond * 500) // for test expired event deleteFn(0) diff --git a/resource-directory/service/projection.go b/resource-directory/service/projection.go index cde5507b1..3b81a23ff 100644 --- a/resource-directory/service/projection.go +++ b/resource-directory/service/projection.go @@ -168,16 +168,52 @@ func (p *Projection) wantToReloadDevice(rl *resourceLinksProjection, hrefFilter return finalReload } -func (p *Projection) loadResourceWithLinks(deviceID string, hrefFilter map[string]bool, typeFilter strings.Set, toReloadDevices strings.Set, onResource func(*Resource) error) error { - isMatchingResource := func(res *commands.Resource) bool { - if len(hrefFilter) > 0 && !hrefFilter[res.GetHref()] { - return false +type resourceFilter struct { + hrefFilter map[string]bool + typeFilter strings.Set +} + +func (rf *resourceFilter) isMatchingResource(href string, resourceTypes []string) bool { + if len(rf.hrefFilter) > 0 && !rf.hrefFilter[href] { + return false + } + if !hasMatchingType(resourceTypes, rf.typeFilter) { + return false + } + return true +} + +func (p *Projection) loadAllDeviceResources(ctx context.Context, deviceID string, hrefFilter map[string]bool, typeFilter strings.Set, onResource func(*Resource) error) error { + rf := resourceFilter{hrefFilter: hrefFilter, typeFilter: typeFilter} + err := p.ForceUpdate(ctx, commands.NewResourceID(deviceID, "")) + if err != nil { + return err + } + p.GroupsModels(func(model eventstore.Model) (wantNext bool) { + rp, ok := model.(*resourceProjection) + if !ok { + return true } - if !hasMatchingType(res.GetResourceTypes(), typeFilter) { - return false + resourceID, resourceTypes := rp.GetMatchingData() + if !rf.isMatchingResource(resourceID.GetHref(), resourceTypes) { + return true } - return true - } + err := onResource(&Resource{ + projection: rp, + Resource: &commands.Resource{ + Href: resourceID.GetHref(), + ResourceTypes: resourceTypes, + Anchor: "ocf://" + resourceID.GetDeviceId(), + DeviceId: resourceID.GetDeviceId(), + }, + }) + return err == nil + }, deviceID) + return nil +} + +func (p *Projection) loadResourceWithLinks(deviceID string, hrefFilter map[string]bool, typeFilter strings.Set, toReloadDevices strings.Set, onResource func(*Resource) error) error { + rf := resourceFilter{hrefFilter: hrefFilter, typeFilter: typeFilter} isSnapShotEvent := func(model eventstore.Model) bool { e, ok := model.(interface{ EventType() string }) if !ok { @@ -191,7 +227,7 @@ func (p *Projection) loadResourceWithLinks(deviceID string, hrefFilter map[strin iterateResources := func(rl *resourceLinksProjection) error { var err error rl.IterateOverResources(func(res *commands.Resource) (wantNext bool) { - if !isMatchingResource(res) { + if !rf.isMatchingResource(res.GetHref(), res.GetResourceTypes()) { return true } p.Models(func(model eventstore.Model) (wantNext bool) { @@ -220,9 +256,16 @@ func (p *Projection) loadResourceWithLinks(deviceID string, hrefFilter map[strin }) } -func (p *Projection) LoadResourcesWithLinks(resourceIDFilter []*commands.ResourceId, typeFilter strings.Set, toReloadDevices strings.Set, onResource func(*Resource) error) error { +func (p *Projection) LoadResources(ctx context.Context, resourceIDFilter []*commands.ResourceId, typeFilter strings.Set, includeHiddenResources bool, toReloadDevices strings.Set, onResource func(*Resource) error) error { resourceIDMapFilter := getResourceIDMapFilter(resourceIDFilter) for deviceID, hrefFilter := range resourceIDMapFilter { // filter duplicit load + if includeHiddenResources { + err := p.loadAllDeviceResources(ctx, deviceID, hrefFilter, typeFilter, onResource) + if err != nil { + return err + } + continue + } err := p.loadResourceWithLinks(deviceID, hrefFilter, typeFilter, toReloadDevices, onResource) if err != nil { return err diff --git a/resource-directory/service/resourceLinksProjection.go b/resource-directory/service/resourceLinksProjection.go index de825dd6d..b54ce30c2 100644 --- a/resource-directory/service/resourceLinksProjection.go +++ b/resource-directory/service/resourceLinksProjection.go @@ -113,9 +113,9 @@ func (rlp *resourceLinksProjection) Handle(ctx context.Context, iter eventstore. } func (rlp *resourceLinksProjection) ToResourceLinksPublished(typeFilter strings.Set) *events.ResourceLinksPublished { - resources := make([]*commands.Resource, 0, len(rlp.private.snapshot.GetResources())) rlp.private.lock.RLock() defer rlp.private.lock.RUnlock() + resources := make([]*commands.Resource, 0, len(rlp.private.snapshot.GetResources())) for _, resource := range rlp.private.snapshot.GetResources() { if hasMatchingType(resource.GetResourceTypes(), typeFilter) { resources = append(resources, resource) diff --git a/resource-directory/service/resourceProjection.go b/resource-directory/service/resourceProjection.go index e4ea61d9a..3092e199a 100644 --- a/resource-directory/service/resourceProjection.go +++ b/resource-directory/service/resourceProjection.go @@ -22,6 +22,7 @@ type resourceProjection struct { content *events.ResourceChanged version uint64 onResourceChangedVersion uint64 + resourceTypes []string resourceUpdatePendings []*events.ResourceUpdatePending resourceRetrievePendings []*events.ResourceRetrievePending resourceDeletePendings []*events.ResourceDeletePending @@ -56,6 +57,7 @@ func (rp *resourceProjection) handleResourceStateSnapshotTakenLocked(eu eventsto } rp.private.resourceID = s.GetResourceId() rp.private.content = s.GetLatestResourceChange() + rp.private.resourceTypes = s.GetResourceTypes() rp.private.onResourceChangedVersion = eu.Version() rp.private.resourceUpdatePendings = s.GetResourceUpdatePendings() rp.private.resourceCreatePendings = s.GetResourceCreatePendings() @@ -64,12 +66,19 @@ func (rp *resourceProjection) handleResourceStateSnapshotTakenLocked(eu eventsto return nil } +func (rp *resourceProjection) GetMatchingData() (*commands.ResourceId, []string) { + rp.private.lock.RLock() + defer rp.private.lock.RUnlock() + return rp.private.resourceID, rp.private.resourceTypes +} + func (rp *resourceProjection) handleResourceChangedLocked(eu eventstore.EventUnmarshaler) error { var s events.ResourceChanged if err := eu.Unmarshal(&s); err != nil { return err } rp.private.resourceID = s.GetResourceId() + rp.setResourceTypesLocked(s.GetResourceTypes()) rp.private.content = &s rp.private.onResourceChangedVersion = eu.Version() return nil @@ -80,6 +89,7 @@ func (rp *resourceProjection) handleResourceUpdatePendingLocked(eu eventstore.Ev if err := eu.Unmarshal(&s); err != nil { return err } + rp.setResourceTypesLocked(s.GetResourceTypes()) rp.private.resourceUpdatePendings = append(rp.private.resourceUpdatePendings, &s) rp.private.resourceID = s.GetResourceId() return nil @@ -90,6 +100,7 @@ func (rp *resourceProjection) handleResourceUpdatedLocked(eu eventstore.EventUnm if err := eu.Unmarshal(&s); err != nil { return err } + rp.setResourceTypesLocked(s.GetResourceTypes()) rp.private.resourceID = s.GetResourceId() tmp := make([]*events.ResourceUpdatePending, 0, 16) var found bool @@ -111,6 +122,7 @@ func (rp *resourceProjection) handleResourceRetrievePendingLocked(eu eventstore. if err := eu.Unmarshal(&s); err != nil { return err } + rp.setResourceTypesLocked(s.GetResourceTypes()) rp.private.resourceID = s.GetResourceId() rp.private.resourceRetrievePendings = append(rp.private.resourceRetrievePendings, &s) return nil @@ -121,6 +133,7 @@ func (rp *resourceProjection) handleResourceDeletePendingLocked(eu eventstore.Ev if err := eu.Unmarshal(&s); err != nil { return err } + rp.setResourceTypesLocked(s.GetResourceTypes()) rp.private.resourceID = s.GetResourceId() rp.private.resourceDeletePendings = append(rp.private.resourceDeletePendings, &s) return nil @@ -131,6 +144,7 @@ func (rp *resourceProjection) handleResourceRetrievedLocked(eu eventstore.EventU if err := eu.Unmarshal(&s); err != nil { return err } + rp.setResourceTypesLocked(s.GetResourceTypes()) rp.private.resourceID = s.GetResourceId() tmp := make([]*events.ResourceRetrievePending, 0, 16) var found bool @@ -152,6 +166,7 @@ func (rp *resourceProjection) handleResourceDeletedLocked(eu eventstore.EventUnm if err := eu.Unmarshal(&s); err != nil { return err } + rp.setResourceTypesLocked(s.GetResourceTypes()) rp.private.resourceID = s.GetResourceId() tmp := make([]*events.ResourceDeletePending, 0, 16) var found bool @@ -173,16 +188,25 @@ func (rp *resourceProjection) handleResourceCreatePendingLocked(eu eventstore.Ev if err := eu.Unmarshal(&s); err != nil { return err } + rp.setResourceTypesLocked(s.GetResourceTypes()) rp.private.resourceCreatePendings = append(rp.private.resourceCreatePendings, &s) rp.private.resourceID = s.GetResourceId() return nil } +func (rp *resourceProjection) setResourceTypesLocked(resourceTypes []string) { + if len(resourceTypes) == 0 { + return + } + rp.private.resourceTypes = resourceTypes +} + func (rp *resourceProjection) handleResourceCreatedLocked(eu eventstore.EventUnmarshaler) error { var s events.ResourceCreated if err := eu.Unmarshal(&s); err != nil { return err } + rp.setResourceTypesLocked(s.GetResourceTypes()) rp.private.resourceID = s.GetResourceId() tmp := make([]*events.ResourceCreatePending, 0, 16) var found bool diff --git a/resource-directory/service/resourceShadow.go b/resource-directory/service/resourceShadow.go index 93b880c68..54eceef5d 100644 --- a/resource-directory/service/resourceShadow.go +++ b/resource-directory/service/resourceShadow.go @@ -2,6 +2,7 @@ package service import ( "bytes" + "context" "time" "github.com/plgd-dev/device/v2/schema/device" @@ -61,10 +62,10 @@ func (rd *ResourceTwin) convertToResourceIDs(resourceIDsFilter []*pb.ResourceIdF return internalResourceIDsFilter } -func (rd *ResourceTwin) filterResources(resourceIDsFilter []*commands.ResourceId, typeFilter []string, toReloadDevices strings.Set, onResource func(*Resource) error) error { +func (rd *ResourceTwin) filterResources(ctx context.Context, resourceIDsFilter []*commands.ResourceId, typeFilter []string, includeHiddenResources bool, toReloadDevices strings.Set, onResource func(*Resource) error) error { mapTypeFilter := make(strings.Set) mapTypeFilter.Add(typeFilter...) - return rd.projection.LoadResourcesWithLinks(resourceIDsFilter, mapTypeFilter, toReloadDevices, onResource) + return rd.projection.LoadResources(ctx, resourceIDsFilter, mapTypeFilter, includeHiddenResources, toReloadDevices, onResource) } func resourceIdFilterToSimple(r []*pb.ResourceIdFilter) []*commands.ResourceId { @@ -112,7 +113,7 @@ func updateContentIfETagMatched(resourceIDsFilter []*pb.ResourceIdFilter, val *p } func (rd *ResourceTwin) getResources(resourceIDsFilter []*pb.ResourceIdFilter, typeFilter []string, srv pb.GrpcGateway_GetResourcesServer, toReloadDevices strings.Set) error { - return rd.filterResources(resourceIdFilterToSimple(resourceIDsFilter), typeFilter, toReloadDevices, func(resource *Resource) error { + return rd.filterResources(srv.Context(), resourceIdFilterToSimple(resourceIDsFilter), typeFilter, false, toReloadDevices, func(resource *Resource) error { val := toResourceValue(resource) updateContentIfETagMatched(resourceIDsFilter, val) err := srv.Send(val) @@ -153,8 +154,8 @@ func toPendingCommands(resource *Resource, commandFilter subscription.FilterBitm return resource.projection.ToPendingCommands(commandFilter, now) } -func (rd *ResourceTwin) sendPendingCommands(srv pb.GrpcGateway_GetPendingCommandsServer, resourceIDsFilter []*pb.ResourceIdFilter, typeFilter []string, filterCmds subscription.FilterBitmask, now time.Time, toReloadDevices strings.Set) error { - return rd.filterResources(resourceIdFilterToSimple(resourceIDsFilter), typeFilter, toReloadDevices, func(resource *Resource) error { +func (rd *ResourceTwin) sendPendingCommands(srv pb.GrpcGateway_GetPendingCommandsServer, resourceIDsFilter []*pb.ResourceIdFilter, typeFilter []string, filterCmds subscription.FilterBitmask, includeHiddenResources bool, now time.Time, toReloadDevices strings.Set) error { + return rd.filterResources(srv.Context(), resourceIdFilterToSimple(resourceIDsFilter), typeFilter, includeHiddenResources, toReloadDevices, func(resource *Resource) error { for _, pendingCmd := range toPendingCommands(resource, filterCmds, now) { err := srv.Send(pendingCmd) if err != nil { @@ -211,7 +212,7 @@ func (rd *ResourceTwin) GetPendingCommands(req *pb.GetPendingCommandsRequest, sr resourceIDsFilter := rd.convertToResourceIDs(req.GetResourceIdFilter(), req.GetDeviceIdFilter()) toReloadDevices := make(strings.Set) - err = rd.sendPendingCommands(srv, resourceIDsFilter, req.GetTypeFilter(), filterCmds, now, toReloadDevices) + err = rd.sendPendingCommands(srv, resourceIDsFilter, req.GetTypeFilter(), filterCmds, req.GetIncludeHiddenResources(), now, toReloadDevices) if err != nil { return err } @@ -223,7 +224,7 @@ func (rd *ResourceTwin) GetPendingCommands(req *pb.GetPendingCommandsRequest, sr newResourceIDsFilter = append(newResourceIDsFilter, resourceIDsFilter[i]) } } - return rd.sendPendingCommands(srv, newResourceIDsFilter, req.GetTypeFilter(), filterCmds, now, nil) + return rd.sendPendingCommands(srv, newResourceIDsFilter, req.GetTypeFilter(), filterCmds, req.GetIncludeHiddenResources(), now, nil) } return nil } diff --git a/snippet-service/Makefile b/snippet-service/Makefile new file mode 100644 index 000000000..25a1a528d --- /dev/null +++ b/snippet-service/Makefile @@ -0,0 +1,64 @@ +SHELL = /bin/bash +SERVICE_NAME = $(notdir $(CURDIR)) +LATEST_TAG ?= vnext +BRANCH_TAG ?= $(shell git rev-parse --abbrev-ref HEAD | sed 's/[^a-zA-Z0-9]/-/g') +ifneq ($(BRANCH_TAG),main) + LATEST_TAG = $(BRANCH_TAG) +endif +VERSION_TAG ?= $(LATEST_TAG)-$(shell git rev-parse --short=7 --verify HEAD) +GOPATH ?= $(shell go env GOPATH) +WORKING_DIRECTORY := $(shell pwd) +REPOSITORY_DIRECTORY := $(shell cd .. && pwd) +BUILD_COMMIT_DATE ?= $(shell date -u +%FT%TZ --date=@`git show --format='%ct' HEAD --quiet`) +BUILD_SHORT_COMMIT ?= $(shell git show --format=%h HEAD --quiet) +BUILD_DATE ?= $(shell date -u +%FT%TZ) +BUILD_VERSION ?= $(shell git tag --sort version:refname | tail -1 | sed -e "s/^v//") + +default: build + +define build-docker-image + cd .. && \ + mkdir -p .tmp/docker/$(SERVICE_NAME) && \ + awk '{gsub("@NAME@","$(SERVICE_NAME)")} {gsub("@DIRECTORY@","$(SERVICE_NAME)")} {print}' tools/docker/Dockerfile.in > .tmp/docker/$(SERVICE_NAME)/Dockerfile && \ + docker build \ + --network=host \ + --tag ghcr.io/plgd-dev/hub/$(SERVICE_NAME):$(VERSION_TAG) \ + --tag ghcr.io/plgd-dev/hub/$(SERVICE_NAME):$(LATEST_TAG) \ + --tag ghcr.io/plgd-dev/hub/$(SERVICE_NAME):$(BRANCH_TAG) \ + --build-arg COMMIT_DATE="$(BUILD_COMMIT_DATE)" \ + --build-arg SHORT_COMMIT="$(BUILD_SHORT_COMMIT)" \ + --build-arg DATE="$(BUILD_DATE)" \ + --build-arg VERSION="$(BUILD_VERSION)" \ + --target $(1) \ + -f .tmp/docker/$(SERVICE_NAME)/Dockerfile \ + . +endef + +build-servicecontainer: + $(call build-docker-image,service) + +build: build-servicecontainer + +push: build-servicecontainer + docker push plgd/$(SERVICE_NAME):$(VERSION_TAG) + docker push plgd/$(SERVICE_NAME):$(LATEST_TAG) + +GOOGLEAPIS_PATH := $(REPOSITORY_DIRECTORY)/dependency/googleapis +GRPCGATEWAY_MODULE_PATH := $(shell go list -m -f '{{.Dir}}' github.com/grpc-ecosystem/grpc-gateway/v2 | head -1) + +proto/generate: + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --go_out=$(GOPATH)/src $(WORKING_DIRECTORY)/pb/service.proto + protoc-go-inject-tag -remove_tag_comment -input=$(WORKING_DIRECTORY)/pb/service.pb.go + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --openapiv2_out=$(REPOSITORY_DIRECTORY) \ + --openapiv2_opt logtostderr=true \ + $(WORKING_DIRECTORY)/pb/service.proto + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --grpc-gateway_out=$(REPOSITORY_DIRECTORY) \ + --grpc-gateway_opt logtostderr=true \ + --grpc-gateway_opt paths=source_relative \ + $(WORKING_DIRECTORY)/pb/service.proto + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --go-grpc_out=$(GOPATH)/src \ + $(WORKING_DIRECTORY)/pb/service.proto + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --doc_out=$(WORKING_DIRECTORY)/pb --doc_opt=markdown,README.md $(WORKING_DIRECTORY)/pb/*.proto + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --doc_out=$(WORKING_DIRECTORY)/pb --doc_opt=html,doc.html $(WORKING_DIRECTORY)/pb/*.proto + +.PHONY: build-servicecontainer build push proto/generate diff --git a/snippet-service/cmd/service/main.go b/snippet-service/cmd/service/main.go new file mode 100644 index 000000000..ce496526c --- /dev/null +++ b/snippet-service/cmd/service/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "fmt" + + "github.com/plgd-dev/hub/v2/pkg/build" + "github.com/plgd-dev/hub/v2/pkg/config" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/snippet-service/service" +) + +func run(cfg service.Config, logger log.Logger) error { + fileWatcher, err := fsnotify.NewWatcher(logger) + if err != nil { + return fmt.Errorf("cannot create file fileWatcher: %w", err) + } + defer func() { + _ = fileWatcher.Close() + }() + + s, err := service.New(context.Background(), cfg, fileWatcher, logger) + if err != nil { + return fmt.Errorf("cannot create service: %w", err) + } + err = s.Serve() + if err != nil { + return fmt.Errorf("cannot serve service: %w", err) + } + + return nil +} + +func main() { + var cfg service.Config + if err := config.LoadAndValidateConfig(&cfg); err != nil { + log.Fatalf("cannot load config: %v", err) + } + logger := log.NewLogger(cfg.Log) + log.Set(logger) + logger.Debugf("version: %v, buildDate: %v, buildRevision %v", build.Version, build.BuildDate, build.CommitHash) + log.Infof("config: %v", cfg.String()) + + if err := run(cfg, logger); err != nil { + log.Fatalf("cannot run service: %v", err) + } +} diff --git a/snippet-service/config.yaml b/snippet-service/config.yaml new file mode 100644 index 000000000..c19f941f7 --- /dev/null +++ b/snippet-service/config.yaml @@ -0,0 +1,107 @@ +hubID: "" +log: + level: info + encoding: json + stacktrace: + enabled: false + level: warn + encoderConfig: + timeEncoder: rfc3339nano +apis: + grpc: + address: "0.0.0.0:9100" + sendMsgSize: 4194304 + recvMsgSize: 4194304 + enforcementPolicy: + minTime: 5s + permitWithoutStream: true + keepAlive: + # 0s - means infinity + maxConnectionIdle: 0s + # 0s - means infinity + maxConnectionAge: 0s + # 0s - means infinity + maxConnectionAgeGrace: 0s + time: 2h + timeout: 20s + tls: + caPool: "/secrets/public/rootca.crt" + keyFile: "/secrets/private/cert.key" + certFile: "/secrets/private/cert.crt" + clientCertificateRequired: true + authorization: + ownerClaim: "sub" + authority: "" + audience: "" + http: + maxIdleConns: 16 + maxConnsPerHost: 32 + maxIdleConnsPerHost: 16 + idleConnTimeout: "30s" + timeout: "10s" + tls: + caPool: "/secrets/public/rootca.crt" + keyFile: "/secrets/private/cert.key" + certFile: "/secrets/public/cert.crt" + useSystemCAPool: false + http: + address: "0.0.0.0:9101" + readTimeout: 8s + readHeaderTimeout: 4s + writeTimeout: 16s + idleTimeout: 30s +clients: + storage: + use: mongoDB + mongoDB: + uri: + database: snippetService + maxPoolSize: 16 + maxConnIdleTime: 4m0s + tls: + caPool: "/secrets/public/rootca.crt" + keyFile: "/secrets/private/cert.key" + certFile: "/secrets/public/cert.crt" + useSystemCAPool: false + openTelemetryCollector: + grpc: + enabled: false + address: "" + sendMsgSize: 4194304 + recvMsgSize: 4194304 + keepAlive: + time: 10s + timeout: 20s + permitWithoutStream: true + tls: + caPool: "/secrets/public/rootca.crt" + keyFile: "/secrets/private/cert.key" + certFile: "/secrets/public/cert.crt" + useSystemCAPool: false + eventBus: + subscriptionID: "snippet-service" + nats: + url: "" + pendingLimits: + msgLimit: 524288 + bytesLimit: 67108864 + tls: + caPool: "/secrets/public/rootca.crt" + keyFile: "/secrets/private/cert.key" + certFile: "/secrets/public/cert.crt" + useSystemCAPool: false + resourceUpdater: + cleanUpExpiredUpdates: "0 * * * *" + grpc: + address: "" + sendMsgSize: 4194304 + recvMsgSize: 4194304 + keepAlive: + time: 10s + timeout: 20s + permitWithoutStream: true + tls: + caPool: "/secrets/public/rootca.crt" + keyFile: "/secrets/private/cert.key" + certFile: "/secrets/public/cert.crt" + useSystemCAPool: false diff --git a/snippet-service/jq/jq.go b/snippet-service/jq/jq.go new file mode 100644 index 000000000..470135e09 --- /dev/null +++ b/snippet-service/jq/jq.go @@ -0,0 +1,23 @@ +package jq + +import ( + "fmt" + + "github.com/itchyny/gojq" +) + +func EvalJQCondition(jq string, v any) (bool, error) { + q, err := gojq.Parse(jq) + if err != nil { + return false, fmt.Errorf("cannot parse jq query(%v): %w", jq, err) + } + iter := q.Run(v) + val, ok := iter.Next() + if !ok { + return false, fmt.Errorf("jq query(%v) returned no result", jq) + } + if result, ok := val.(bool); ok { + return result, nil + } + return false, fmt.Errorf("invalid jq result: %v", val) +} diff --git a/snippet-service/jq/jq_test.go b/snippet-service/jq/jq_test.go new file mode 100644 index 000000000..5b41aa707 --- /dev/null +++ b/snippet-service/jq/jq_test.go @@ -0,0 +1,129 @@ +package jq_test + +import ( + "testing" + + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/snippet-service/jq" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/kit/v2/codec/json" + "github.com/stretchr/testify/require" +) + +func TestEvalJQCondition(t *testing.T) { + tests := []struct { + name string + jq string + content *commands.Content + wantErr bool + want bool + }{ + { + name: "invalid jq", + jq: "][", + content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "value": 42, + }), + }, + wantErr: true, + }, + { + name: "invalid jq returned type", // we expect a boolean + jq: ".", + content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "value": 42, + }), + }, + wantErr: true, + }, + { + name: "nonexisting attribute", + jq: ".nonexisting == 42", + content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "value": 42, + }), + }, + want: false, + }, + { + name: "int", + jq: ". == 42", + content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, 42), + }, + want: true, + }, + { + name: "string", + jq: ". == \"leet\"", + content: &commands.Content{ + ContentType: message.AppJSON.String(), + Data: func() []byte { + data, err := json.Encode("leet") + require.NoError(t, err) + return data + }(), + }, + want: true, + }, + { + name: "array", + jq: ". == [1, 2, 3]", + content: &commands.Content{ + ContentType: message.AppJSON.String(), + Data: func() []byte { + data, err := json.Encode([]int{1, 2, 3}) + require.NoError(t, err) + return data + }(), + }, + want: true, + }, + { + name: "object", + jq: ".a == 42 and .b == \"leet\" and .c[1] == 2", + content: &commands.Content{ + ContentType: message.AppJSON.String(), + Data: func() []byte { + data, err := json.Encode(map[string]any{ + "a": 42, + "b": "leet", + "c": []int{1, 2, 3}, + }) + require.NoError(t, err) + return data + }(), + }, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var json any + var jsonMap map[string]any + err := commands.DecodeContent(tt.content, &jsonMap) + if err == nil { + json = jsonMap + } else { + err = commands.DecodeContent(tt.content, &json) + } + require.NoError(t, err) + got, err := jq.EvalJQCondition(tt.jq, json) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/snippet-service/pb/README.md b/snippet-service/pb/README.md new file mode 100644 index 000000000..de11be806 --- /dev/null +++ b/snippet-service/pb/README.md @@ -0,0 +1,424 @@ +# Protocol Documentation + + +## Table of Contents + +- [snippet-service/pb/service.proto](#snippet-service_pb_service-proto) + - [AppliedConfiguration](#snippetservice-pb-AppliedConfiguration) + - [AppliedConfiguration.LinkedTo](#snippetservice-pb-AppliedConfiguration-LinkedTo) + - [AppliedConfiguration.Resource](#snippetservice-pb-AppliedConfiguration-Resource) + - [Condition](#snippetservice-pb-Condition) + - [Configuration](#snippetservice-pb-Configuration) + - [Configuration.Resource](#snippetservice-pb-Configuration-Resource) + - [DeleteAppliedConfigurationsRequest](#snippetservice-pb-DeleteAppliedConfigurationsRequest) + - [DeleteAppliedConfigurationsResponse](#snippetservice-pb-DeleteAppliedConfigurationsResponse) + - [DeleteConditionsRequest](#snippetservice-pb-DeleteConditionsRequest) + - [DeleteConditionsResponse](#snippetservice-pb-DeleteConditionsResponse) + - [DeleteConfigurationsRequest](#snippetservice-pb-DeleteConfigurationsRequest) + - [DeleteConfigurationsResponse](#snippetservice-pb-DeleteConfigurationsResponse) + - [GetAppliedConfigurationsRequest](#snippetservice-pb-GetAppliedConfigurationsRequest) + - [GetConditionsRequest](#snippetservice-pb-GetConditionsRequest) + - [GetConfigurationsRequest](#snippetservice-pb-GetConfigurationsRequest) + - [IDFilter](#snippetservice-pb-IDFilter) + - [InvokeConfigurationRequest](#snippetservice-pb-InvokeConfigurationRequest) + - [InvokeConfigurationResponse](#snippetservice-pb-InvokeConfigurationResponse) + + - [AppliedConfiguration.Resource.Status](#snippetservice-pb-AppliedConfiguration-Resource-Status) + + - [SnippetService](#snippetservice-pb-SnippetService) + +- [Scalar Value Types](#scalar-value-types) + + + + +

Top

+ +## snippet-service/pb/service.proto + + + + + +### AppliedConfiguration + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id | [string](#string) | | | +| device_id | [string](#string) | | @gotags: bson:"deviceId" | +| configuration_id | [AppliedConfiguration.LinkedTo](#snippetservice-pb-AppliedConfiguration-LinkedTo) | | @gotags: bson:"configurationId" | +| on_demand | [bool](#bool) | | | +| condition_id | [AppliedConfiguration.LinkedTo](#snippetservice-pb-AppliedConfiguration-LinkedTo) | | @gotags: bson:"conditionId" | +| resources | [AppliedConfiguration.Resource](#snippetservice-pb-AppliedConfiguration-Resource) | repeated | | +| owner | [string](#string) | | | +| timestamp | [int64](#int64) | | Unix timestamp in ns when the applied device configuration has been created/updated | + + + + + + + + +### AppliedConfiguration.LinkedTo + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id | [string](#string) | | | +| version | [uint64](#uint64) | | | + + + + + + + + +### AppliedConfiguration.Resource + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| href | [string](#string) | | | +| correlation_id | [string](#string) | | Reused from invoke command or generated. Can be used to retrieve corresponding pending command. + +@gotags: bson:"correlationId" | +| status | [AppliedConfiguration.Resource.Status](#snippetservice-pb-AppliedConfiguration-Resource-Status) | | | +| resource_updated | [resourceaggregate.pb.ResourceUpdated](#resourceaggregate-pb-ResourceUpdated) | | @gotags: bson:"resourceUpdated,omitempty" | +| valid_until | [int64](#int64) | | Unix nanoseconds timestamp for resource in PENDING status, until which the pending update is valid + +@gotags: bson:"validUntil,omitempty" | + + + + + + + + +### Condition +driven by resource change event + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id | [string](#string) | | Condition ID | +| version | [uint64](#uint64) | | Condition version | +| name | [string](#string) | | User-friendly condition name | +| enabled | [bool](#bool) | | Condition is enabled/disabled | +| configuration_id | [string](#string) | | ID of the configuration to be applied when the condition is satisfied | +| device_id_filter | [string](#string) | repeated | list of device IDs to which the condition applies | +| resource_type_filter | [string](#string) | repeated | | +| resource_href_filter | [string](#string) | repeated | list of resource hrefs to which the condition applies | +| jq_expression_filter | [string](#string) | | | +| api_access_token | [string](#string) | | Token used to update resources in the configuration | +| owner | [string](#string) | | Condition owner | +| timestamp | [int64](#int64) | | Unix timestamp in ns when the condition has been created/updated | + + + + + + + + +### Configuration + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id | [string](#string) | | Configuration ID | +| version | [uint64](#uint64) | | Configuration version | +| name | [string](#string) | | User-friendly configuration name | +| resources | [Configuration.Resource](#snippetservice-pb-Configuration-Resource) | repeated | List of resource updates to be applied | +| owner | [string](#string) | | Configuration owner | +| timestamp | [int64](#int64) | | Unix timestamp in ns when the configuration has been created/updated | + + + + + + + + +### Configuration.Resource + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| href | [string](#string) | | href of the resource | +| content | [resourceaggregate.pb.Content](#resourceaggregate-pb-Content) | | content update of the resource | +| time_to_live | [int64](#int64) | | optional update command time to live, 0 is infinite | + + + + + + + + +### DeleteAppliedConfigurationsRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id_filter | [string](#string) | repeated | | + + + + + + + + +### DeleteAppliedConfigurationsResponse + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| success | [bool](#bool) | | | + + + + + + + + +### DeleteConditionsRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id_filter | [IDFilter](#snippetservice-pb-IDFilter) | repeated | | +| http_id_filter | [string](#string) | repeated | **Deprecated.** Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" | + + + + + + + + +### DeleteConditionsResponse + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| success | [bool](#bool) | | | + + + + + + + + +### DeleteConfigurationsRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id_filter | [IDFilter](#snippetservice-pb-IDFilter) | repeated | | +| http_id_filter | [string](#string) | repeated | **Deprecated.** Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" | + + + + + + + + +### DeleteConfigurationsResponse + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| success | [bool](#bool) | | | + + + + + + + + +### GetAppliedConfigurationsRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id_filter | [string](#string) | repeated | | +| device_id_filter | [string](#string) | repeated | | +| configuration_id_filter | [IDFilter](#snippetservice-pb-IDFilter) | repeated | | +| http_configuration_id_filter | [string](#string) | repeated | **Deprecated.** Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" | +| condition_id_filter | [IDFilter](#snippetservice-pb-IDFilter) | repeated | | +| http_condition_id_filter | [string](#string) | repeated | **Deprecated.** Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" | + + + + + + + + +### GetConditionsRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id_filter | [IDFilter](#snippetservice-pb-IDFilter) | repeated | | +| configuration_id_filter | [string](#string) | repeated | returns latest conditions for given configurationId | +| http_id_filter | [string](#string) | repeated | **Deprecated.** Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" | + + + + + + + + +### GetConfigurationsRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id_filter | [IDFilter](#snippetservice-pb-IDFilter) | repeated | | +| http_id_filter | [string](#string) | repeated | **Deprecated.** Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" | + + + + + + + + +### IDFilter + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id | [string](#string) | | | +| value | [uint64](#uint64) | | | +| all | [bool](#bool) | | | +| latest | [bool](#bool) | | | + + + + + + + + +### InvokeConfigurationRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| configuration_id | [string](#string) | | applies latest configuration | +| device_id | [string](#string) | | | +| force | [bool](#bool) | | force update even if the configuration has already been applied to device | +| correlation_id | [string](#string) | | propagated down to the resource update command | + + + + + + + + +### InvokeConfigurationResponse + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| applied_configuration_id | [string](#string) | | | + + + + + + + + + + +### AppliedConfiguration.Resource.Status + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| UNSPECIFIED | 0 | | +| QUEUED | 1 | | +| PENDING | 2 | | +| DONE | 3 | If done look to resource_updated if update resource failed for resource aggregate. | +| TIMEOUT | 4 | | + + + + + + + + + +### SnippetService + + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------ | ------------- | ------------| +| CreateCondition | [Condition](#snippetservice-pb-Condition) | [Condition](#snippetservice-pb-Condition) | | +| GetConditions | [GetConditionsRequest](#snippetservice-pb-GetConditionsRequest) | [Condition](#snippetservice-pb-Condition) stream | | +| DeleteConditions | [DeleteConditionsRequest](#snippetservice-pb-DeleteConditionsRequest) | [DeleteConditionsResponse](#snippetservice-pb-DeleteConditionsResponse) | | +| UpdateCondition | [Condition](#snippetservice-pb-Condition) | [Condition](#snippetservice-pb-Condition) | For update the condition whole condition is required and the version must be incremented. | +| CreateConfiguration | [Configuration](#snippetservice-pb-Configuration) | [Configuration](#snippetservice-pb-Configuration) | | +| GetConfigurations | [GetConfigurationsRequest](#snippetservice-pb-GetConfigurationsRequest) | [Configuration](#snippetservice-pb-Configuration) stream | | +| DeleteConfigurations | [DeleteConfigurationsRequest](#snippetservice-pb-DeleteConfigurationsRequest) | [DeleteConfigurationsResponse](#snippetservice-pb-DeleteConfigurationsResponse) | | +| UpdateConfiguration | [Configuration](#snippetservice-pb-Configuration) | [Configuration](#snippetservice-pb-Configuration) | For update the configuration whole configuration is required and the version must be incremented. | +| InvokeConfiguration | [InvokeConfigurationRequest](#snippetservice-pb-InvokeConfigurationRequest) | [InvokeConfigurationResponse](#snippetservice-pb-InvokeConfigurationResponse) | streaming process of update configuration to invoker | +| GetAppliedConfigurations | [GetAppliedConfigurationsRequest](#snippetservice-pb-GetAppliedConfigurationsRequest) | [AppliedConfiguration](#snippetservice-pb-AppliedConfiguration) stream | | +| DeleteAppliedConfigurations | [DeleteAppliedConfigurationsRequest](#snippetservice-pb-DeleteAppliedConfigurationsRequest) | [DeleteAppliedConfigurationsResponse](#snippetservice-pb-DeleteAppliedConfigurationsResponse) | | + + + + + +## Scalar Value Types + +| .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby | +| ----------- | ----- | --- | ---- | ------ | -- | -- | --- | ---- | +| double | | double | double | float | float64 | double | float | Float | +| float | | float | float | float | float32 | float | float | Float | +| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) | +| uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) | +| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) | +| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum | +| sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| bool | | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | +| string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) | +| bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) | + diff --git a/snippet-service/pb/appliedConfiguration.go b/snippet-service/pb/appliedConfiguration.go new file mode 100644 index 000000000..813be74dd --- /dev/null +++ b/snippet-service/pb/appliedConfiguration.go @@ -0,0 +1,144 @@ +package pb + +import ( + "errors" + "fmt" + + "github.com/google/uuid" + pkgMongo "github.com/plgd-dev/hub/v2/pkg/mongodb" +) + +func (r *AppliedConfiguration_Resource) Validate() error { + if r.GetHref() == "" { + return errors.New("missing href") + } + if r.GetCorrelationId() == "" { + return errors.New("missing correlationID") + } + if r.GetStatus() == AppliedConfiguration_Resource_UNSPECIFIED { + return errors.New("invalid status") + } + return nil +} + +func (c *AppliedConfiguration) Validate() error { + if c.GetId() != "" { + if _, err := uuid.Parse(c.GetId()); err != nil { + return fmt.Errorf("invalid ID(%v): %w", c.GetId(), err) + } + } + if c.GetOwner() == "" { + return errors.New("missing owner") + } + if c.GetDeviceId() == "" { + return errors.New("missing deviceID") + } + if c.GetConfigurationId().GetId() == "" { + return errors.New("invalid configurationID") + } + if c.GetExecutedBy() == nil { + return errors.New("missing executedBy") + } + if len(c.GetResources()) == 0 { + return errors.New("missing resources") + } + for _, r := range c.GetResources() { + if err := r.Validate(); err != nil { + return fmt.Errorf("invalid resource: %w", err) + } + } + return nil +} + +func MakeLinkedTo(id string, version uint64) *AppliedConfiguration_LinkedTo { + return &AppliedConfiguration_LinkedTo{ + Id: id, + Version: version, + } +} + +func (r *AppliedConfiguration_LinkedTo) Clone() *AppliedConfiguration_LinkedTo { + if r == nil { + return nil + } + return &AppliedConfiguration_LinkedTo{ + Id: r.GetId(), + Version: r.GetVersion(), + } +} + +func MakeExecutedByOnDemand() *AppliedConfiguration_OnDemand { + return &AppliedConfiguration_OnDemand{ + OnDemand: true, + } +} + +func MakeExecutedByConditionId(conditionID string, version uint64) *AppliedConfiguration_ConditionId { + return &AppliedConfiguration_ConditionId{ + ConditionId: &AppliedConfiguration_LinkedTo{ + Id: conditionID, + Version: version, + }, + } +} + +func (r *AppliedConfiguration_Resource) Clone() *AppliedConfiguration_Resource { + return &AppliedConfiguration_Resource{ + Href: r.GetHref(), + CorrelationId: r.GetCorrelationId(), + Status: r.GetStatus(), + ResourceUpdated: r.GetResourceUpdated().Clone(), + ValidUntil: r.GetValidUntil(), + } +} + +func (r *AppliedConfiguration_Resource) jsonToBSONTag(json map[string]interface{}) { + pkgMongo.ConvertStringValueToInt64(json, "validUntil") +} + +func (r *AppliedConfiguration_Resource) MarshalBSON() ([]byte, error) { + return pkgMongo.MarshalProtoBSON(r, r.jsonToBSONTag) +} + +func (c *AppliedConfiguration) CloneExecutedBy() isAppliedConfiguration_ExecutedBy { + var executedBy isAppliedConfiguration_ExecutedBy + if c.GetOnDemand() { + executedBy = MakeExecutedByOnDemand() + } else if rt := c.GetConditionId(); rt != nil { + executedBy = MakeExecutedByConditionId(rt.GetId(), rt.GetVersion()) + } + return executedBy +} + +func (c *AppliedConfiguration) CloneAppliedConfiguration_Resources() []*AppliedConfiguration_Resource { + var resources []*AppliedConfiguration_Resource + if len(c.GetResources()) > 0 { + resources = make([]*AppliedConfiguration_Resource, 0, len(c.GetResources())) + for _, r := range c.GetResources() { + resources = append(resources, r.Clone()) + } + } + return resources +} + +func (c *AppliedConfiguration) Clone() *AppliedConfiguration { + return &AppliedConfiguration{ + Id: c.GetId(), + DeviceId: c.GetDeviceId(), + ConfigurationId: c.GetConfigurationId().Clone(), + ExecutedBy: c.CloneExecutedBy(), + Resources: c.CloneAppliedConfiguration_Resources(), + Owner: c.GetOwner(), + Timestamp: c.GetTimestamp(), + } +} + +func (c *AppliedConfiguration) jsonToBSONTag(json map[string]interface{}) { + pkgMongo.ConvertStringValueToInt64(json, "configurationId.version") + pkgMongo.ConvertStringValueToInt64(json, "conditionId.version") + pkgMongo.ConvertStringValueToInt64(json, "resources.validUntil") +} + +func (c *AppliedConfiguration) MarshalBSON() ([]byte, error) { + return pkgMongo.MarshalProtoBSON(c, c.jsonToBSONTag) +} diff --git a/snippet-service/pb/condition.go b/snippet-service/pb/condition.go new file mode 100644 index 000000000..bdd9991a7 --- /dev/null +++ b/snippet-service/pb/condition.go @@ -0,0 +1,59 @@ +package pb + +import ( + "errors" + "fmt" + "slices" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/pkg/strings" +) + +func checkConfigurationId(c string, isUpdate bool) error { + if isUpdate && c == "" { + // in this case the update will keep the configuration ID already in the database + return nil + } + if _, err := uuid.Parse(c); err != nil { + return fmt.Errorf("invalid configuration ID(%v): %w", c, err) + } + return nil +} + +func (c *Condition) Validate(isUpdate bool) error { + if isUpdate || c.GetId() != "" { + if _, err := uuid.Parse(c.GetId()); err != nil { + return fmt.Errorf("invalid ID(%v): %w", c.GetId(), err) + } + } + if err := checkConfigurationId(c.GetConfigurationId(), isUpdate); err != nil { + return err + } + if c.GetOwner() == "" { + return errors.New("missing owner") + } + return nil +} + +func (c *Condition) Normalize() { + c.DeviceIdFilter = strings.Unique(c.GetDeviceIdFilter()) + c.ResourceTypeFilter = strings.Unique(c.GetResourceTypeFilter()) + c.ResourceHrefFilter = strings.Unique(c.GetResourceHrefFilter()) +} + +func (c *Condition) Clone() *Condition { + return &Condition{ + Id: c.GetId(), + Name: c.GetName(), + Enabled: c.GetEnabled(), + Owner: c.GetOwner(), + ConfigurationId: c.GetConfigurationId(), + ApiAccessToken: c.GetApiAccessToken(), + Timestamp: c.GetTimestamp(), + Version: c.GetVersion(), + DeviceIdFilter: slices.Clone(c.GetDeviceIdFilter()), + ResourceTypeFilter: slices.Clone(c.GetResourceTypeFilter()), + ResourceHrefFilter: slices.Clone(c.GetResourceHrefFilter()), + JqExpressionFilter: c.GetJqExpressionFilter(), + } +} diff --git a/snippet-service/pb/configuration.go b/snippet-service/pb/configuration.go new file mode 100644 index 000000000..197350ca8 --- /dev/null +++ b/snippet-service/pb/configuration.go @@ -0,0 +1,70 @@ +package pb + +import ( + "errors" + "fmt" + "slices" + "strings" + + "github.com/google/uuid" +) + +func (cr *Configuration_Resource) Clone() *Configuration_Resource { + return &Configuration_Resource{ + Href: cr.GetHref(), + Content: cr.GetContent().Clone(), + TimeToLive: cr.GetTimeToLive(), + } +} + +func (c *Configuration) Validate(isUpdate bool) error { + if isUpdate || c.GetId() != "" { + if _, err := uuid.Parse(c.GetId()); err != nil { + return fmt.Errorf("invalid ID(%v): %w", c.GetId(), err) + } + } + if c.GetOwner() == "" { + return errors.New("missing owner") + } + if len(c.GetResources()) == 0 { + return errors.New("missing resources") + } + return nil +} + +func normalizeResources(resources []*Configuration_Resource) []*Configuration_Resource { + slices.SortFunc(resources, func(i, j *Configuration_Resource) int { + return strings.Compare(i.GetHref(), j.GetHref()) + }) + return slices.CompactFunc(resources, func(i, j *Configuration_Resource) bool { + return i.GetHref() == j.GetHref() + }) +} + +func (c *Configuration) Normalize() { + c.Resources = normalizeResources(c.GetResources()) +} + +func (c *Configuration) Clone() *Configuration { + cfg := &Configuration{ + Id: c.GetId(), + Version: c.GetVersion(), + Name: c.GetName(), + Owner: c.GetOwner(), + Timestamp: c.GetTimestamp(), + } + for _, r := range c.GetResources() { + cfg.Resources = append(cfg.Resources, r.Clone()) + } + return cfg +} + +func (r *InvokeConfigurationRequest) Validate() error { + if r.GetConfigurationId() == "" { + return errors.New("missing configuration ID") + } + if r.GetDeviceId() == "" { + return errors.New("missing device ID") + } + return nil +} diff --git a/snippet-service/pb/doc.html b/snippet-service/pb/doc.html new file mode 100644 index 000000000..4f08136c0 --- /dev/null +++ b/snippet-service/pb/doc.html @@ -0,0 +1,1604 @@ + + + + + Protocol Documentation + + + + + + + + + + +

Protocol Documentation

+ +

Table of Contents

+ +
+ +
+ + + +
+

snippet-service/pb/service.proto

Top +
+

+ + +

AppliedConfiguration

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idstring

device_idstring

@gotags: bson:"deviceId"

configuration_idAppliedConfiguration.LinkedTo

@gotags: bson:"configurationId"

on_demandbool

condition_idAppliedConfiguration.LinkedTo

@gotags: bson:"conditionId"

resourcesAppliedConfiguration.Resourcerepeated

ownerstring

timestampint64

Unix timestamp in ns when the applied device configuration has been created/updated

+ + + + + +

AppliedConfiguration.LinkedTo

+

+ + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idstring

versionuint64

+ + + + + +

AppliedConfiguration.Resource

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
hrefstring

correlation_idstring

Reused from invoke command or generated. Can be used to retrieve corresponding pending command. + +@gotags: bson:"correlationId"

statusAppliedConfiguration.Resource.Status

resource_updatedresourceaggregate.pb.ResourceUpdated

@gotags: bson:"resourceUpdated,omitempty"

valid_untilint64

Unix nanoseconds timestamp for resource in PENDING status, until which the pending update is valid + +@gotags: bson:"validUntil,omitempty"

+ + + + + +

Condition

+

driven by resource change event

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idstring

Condition ID

versionuint64

Condition version

namestring

User-friendly condition name

enabledbool

Condition is enabled/disabled

configuration_idstring

ID of the configuration to be applied when the condition is satisfied

device_id_filterstringrepeated

list of device IDs to which the condition applies

resource_type_filterstringrepeated

resource_href_filterstringrepeated

list of resource hrefs to which the condition applies

jq_expression_filterstring

api_access_tokenstring

Token used to update resources in the configuration

ownerstring

Condition owner

timestampint64

Unix timestamp in ns when the condition has been created/updated

+ + + + + +

Configuration

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idstring

Configuration ID

versionuint64

Configuration version

namestring

User-friendly configuration name

resourcesConfiguration.Resourcerepeated

List of resource updates to be applied

ownerstring

Configuration owner

timestampint64

Unix timestamp in ns when the configuration has been created/updated

+ + + + + +

Configuration.Resource

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
hrefstring

href of the resource

contentresourceaggregate.pb.Content

content update of the resource

time_to_liveint64

optional update command time to live, 0 is infinite

+ + + + + +

DeleteAppliedConfigurationsRequest

+

+ + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
id_filterstringrepeated

+ + + + + +

DeleteAppliedConfigurationsResponse

+

+ + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
successbool

+ + + + + +

DeleteConditionsRequest

+

+ + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
id_filterIDFilterrepeated

http_id_filterstringrepeated

Deprecated. Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234"

+ + + + +

Fields with deprecated option

+ + + + + + + + + + + + + + + +
NameOption
http_id_filter

true

+ + + + + +

DeleteConditionsResponse

+

+ + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
successbool

+ + + + + +

DeleteConfigurationsRequest

+

+ + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
id_filterIDFilterrepeated

http_id_filterstringrepeated

Deprecated. Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234"

+ + + + +

Fields with deprecated option

+ + + + + + + + + + + + + + + +
NameOption
http_id_filter

true

+ + + + + +

DeleteConfigurationsResponse

+

+ + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
successbool

+ + + + + +

GetAppliedConfigurationsRequest

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
id_filterstringrepeated

device_id_filterstringrepeated

configuration_id_filterIDFilterrepeated

http_configuration_id_filterstringrepeated

Deprecated. Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234"

condition_id_filterIDFilterrepeated

http_condition_id_filterstringrepeated

Deprecated. Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234"

+ + + + +

Fields with deprecated option

+ + + + + + + + + + + + + + + + + + + + +
NameOption
http_configuration_id_filter

true

http_condition_id_filter

true

+ + + + + +

GetConditionsRequest

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
id_filterIDFilterrepeated

configuration_id_filterstringrepeated

returns latest conditions for given configurationId

http_id_filterstringrepeated

Deprecated. Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234"

+ + + + +

Fields with deprecated option

+ + + + + + + + + + + + + + + +
NameOption
http_id_filter

true

+ + + + + +

GetConfigurationsRequest

+

+ + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
id_filterIDFilterrepeated

http_id_filterstringrepeated

Deprecated. Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234"

+ + + + +

Fields with deprecated option

+ + + + + + + + + + + + + + + +
NameOption
http_id_filter

true

+ + + + + +

IDFilter

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idstring

valueuint64

allbool

latestbool

+ + + + + +

InvokeConfigurationRequest

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
configuration_idstring

applies latest configuration

device_idstring

forcebool

force update even if the configuration has already been applied to device

correlation_idstring

propagated down to the resource update command

+ + + + + +

InvokeConfigurationResponse

+

+ + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
applied_configuration_idstring

+ + + + + + + +

AppliedConfiguration.Resource.Status

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameNumberDescription
UNSPECIFIED0

QUEUED1

PENDING2

DONE3

If done look to resource_updated if update resource failed for resource aggregate.

TIMEOUT4

+ + + + + +

SnippetService

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Method NameRequest TypeResponse TypeDescription
CreateConditionConditionCondition

GetConditionsGetConditionsRequestCondition stream

DeleteConditionsDeleteConditionsRequestDeleteConditionsResponse

UpdateConditionConditionCondition

For update the condition whole condition is required and the version must be incremented.

CreateConfigurationConfigurationConfiguration

GetConfigurationsGetConfigurationsRequestConfiguration stream

DeleteConfigurationsDeleteConfigurationsRequestDeleteConfigurationsResponse

UpdateConfigurationConfigurationConfiguration

For update the configuration whole configuration is required and the version must be incremented.

InvokeConfigurationInvokeConfigurationRequestInvokeConfigurationResponse

streaming process of update configuration to invoker

GetAppliedConfigurationsGetAppliedConfigurationsRequestAppliedConfiguration stream

DeleteAppliedConfigurationsDeleteAppliedConfigurationsRequestDeleteAppliedConfigurationsResponse

+ + + + +

Methods with HTTP bindings

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Method NameMethodPatternBody
CreateConditionPOST/snippet-service/api/v1/conditions*
GetConditionsGET/snippet-service/api/v1/conditions
DeleteConditionsDELETE/snippet-service/api/v1/conditions
UpdateConditionPUT/snippet-service/api/v1/conditions/{id}*
CreateConfigurationPOST/snippet-service/api/v1/configurations*
GetConfigurationsGET/snippet-service/api/v1/configurations
DeleteConfigurationsDELETE/snippet-service/api/v1/configurations
UpdateConfigurationPUT/snippet-service/api/v1/configurations/{id}*
InvokeConfigurationPOST/snippet-service/api/v1/configurations/{configuration_id}*
GetAppliedConfigurationsGET/snippet-service/api/v1/configurations/applied
DeleteAppliedConfigurationsDELETE/snippet-service/api/v1/configurations/applied
+ + + + +

Scalar Value Types

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.proto TypeNotesC++JavaPythonGoC#PHPRuby
doubledoubledoublefloatfloat64doublefloatFloat
floatfloatfloatfloatfloat32floatfloatFloat
int32Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.int32intintint32intintegerBignum or Fixnum (as required)
int64Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.int64longint/longint64longinteger/stringBignum
uint32Uses variable-length encoding.uint32intint/longuint32uintintegerBignum or Fixnum (as required)
uint64Uses variable-length encoding.uint64longint/longuint64ulonginteger/stringBignum or Fixnum (as required)
sint32Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.int32intintint32intintegerBignum or Fixnum (as required)
sint64Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.int64longint/longint64longinteger/stringBignum
fixed32Always four bytes. More efficient than uint32 if values are often greater than 2^28.uint32intintuint32uintintegerBignum or Fixnum (as required)
fixed64Always eight bytes. More efficient than uint64 if values are often greater than 2^56.uint64longint/longuint64ulonginteger/stringBignum
sfixed32Always four bytes.int32intintint32intintegerBignum or Fixnum (as required)
sfixed64Always eight bytes.int64longint/longint64longinteger/stringBignum
boolboolbooleanbooleanboolboolbooleanTrueClass/FalseClass
stringA string must always contain UTF-8 encoded or 7-bit ASCII text.stringStringstr/unicodestringstringstringString (UTF-8)
bytesMay contain any arbitrary sequence of bytes.stringByteStringstr[]byteByteStringstringString (ASCII-8BIT)
+ + + diff --git a/snippet-service/pb/idfilter.go b/snippet-service/pb/idfilter.go new file mode 100644 index 000000000..e6803a057 --- /dev/null +++ b/snippet-service/pb/idfilter.go @@ -0,0 +1,271 @@ +package pb + +import ( + "cmp" + "slices" + "strconv" + "strings" +) + +func compareIdFilter(i, j *IDFilter) int { + // compare by Id first + if i.GetId() != j.GetId() { + return strings.Compare(i.GetId(), j.GetId()) + } + // then by type + // All is always first + if i.GetAll() { + if j.GetAll() { + return 0 + } + return -1 + } + // Latest is always second + if i.GetLatest() { + if j.GetAll() { + return 1 + } + if j.GetLatest() { + return 0 + } + return -1 + } + // Values are always last, ordered by stored value + if j.GetAll() || j.GetLatest() { + return 1 + } + return cmp.Compare(i.GetValue(), j.GetValue()) +} + +func checkEmptyIdFilter(idfilter []*IDFilter) []*IDFilter { + // if an empty query is provided, return all + if len(idfilter) == 0 { + return nil + } + // if the first filter is All, we can ignore all other filters + first := idfilter[0] + if first.GetId() == "" && first.GetAll() { + return nil + } + return idfilter +} + +// Normalizing the IDFilter entails: +// - ordering the filters: +// - primarily by ID +// - secondarily by type (All, Latest, Value) +// - tertiary for Value type by value +// +// - removing duplicates +// - removing filters that are redundant due to other filters (eg if All is specified, no other filters are needed) +func NormalizeIdFilter(idfilter []*IDFilter) []*IDFilter { + slices.SortFunc(idfilter, compareIdFilter) + idfilter = checkEmptyIdFilter(idfilter) + if len(idfilter) == 0 { + return nil + } + + updatedFilter := make([]*IDFilter, 0) + var idAll bool + var idLatest bool + var idValue bool + var idValueVersion uint64 + setNextLatest := func(idf *IDFilter) { + // we already have the latest filter + if idLatest { + // skip + return + } + idLatest = true + updatedFilter = append(updatedFilter, idf) + } + setNextValue := func(idf *IDFilter) { + value := idf.GetValue() + if idValue && value == idValueVersion { + // skip + return + } + idValue = true + idValueVersion = value + updatedFilter = append(updatedFilter, idf) + } + prevID := "" + for _, idf := range idfilter { + if idf.GetId() != prevID { + idAll = idf.GetAll() + idLatest = idf.GetLatest() + idValue = !idAll && !idLatest + idValueVersion = idf.GetValue() + updatedFilter = append(updatedFilter, idf) + } + + if idAll { + goto next + } + + if idf.GetLatest() { + setNextLatest(idf) + goto next + } + + setNextValue(idf) + + next: + prevID = idf.GetId() + } + return updatedFilter +} + +type VersionFilter struct { + // document IDs for which we want full documents + All []string + // document IDs for which we want the latest version + Latest []string + // map of document IDs and specific versions we want + Versions map[string][]uint64 +} + +func (vf *VersionFilter) IsEmpty() bool { + return len(vf.All) == 0 && len(vf.Latest) == 0 && len(vf.Versions) == 0 +} + +func PartitionIDFilter(idfilter []*IDFilter) VersionFilter { + idFilter := NormalizeIdFilter(idfilter) + if len(idFilter) == 0 { + return VersionFilter{} + } + + vf := VersionFilter{ + Versions: make(map[string][]uint64), + } + + // empty ID ("") is interpreted as filtering by ID disabled, therefore we return all IDs + // if we requested latest for all ids then we can skip specific IDs requesting latest + hasAllIdsLatest := false + // if we requested a specific version for all IDs then we can skip specific IDs requesting that version + hasAllIdsVersion := func(version uint64) bool { + allVersions, ok := vf.Versions[""] + return ok && slices.Contains(allVersions, version) + } + + for _, idf := range idFilter { + if idf.GetAll() { + vf.All = append(vf.All, idf.GetId()) + continue + } + + if idf.GetLatest() { + if hasAllIdsLatest { + continue + } + if idf.GetId() == "" { + hasAllIdsLatest = true + } + vf.Latest = append(vf.Latest, idf.GetId()) + continue + } + + version := idf.GetValue() + if hasAllIdsVersion(version) { + continue + } + idVersions := vf.Versions[idf.GetId()] + idVersions = append(idVersions, version) + vf.Versions[idf.GetId()] = idVersions + } + return vf +} + +func parseVersion(v string) isIDFilter_Version { + switch v { + case "", "all": + return &IDFilter_All{ + All: true, + } + case "latest": + return &IDFilter_Latest{ + Latest: true, + } + default: + ver, err := strconv.ParseUint(v, 10, 64) + if err != nil { + return nil + } + return &IDFilter_Value{ + Value: ver, + } + } +} + +// we are permissive in parsing id filter +func idFilterFromString(v string) *IDFilter { + if len(v) == 0 { + return nil + } + for len(v) > 0 && v[0] == '/' { + v = v[1:] + } + idHref := strings.SplitN(v, "/", 2) + if len(idHref) < 2 { + ver := parseVersion(v) + if ver != nil { + return &IDFilter{ + Version: ver, + } + } + return &IDFilter{ + Id: v, + Version: &IDFilter_All{ + All: true, + }, + } + } + + ver := parseVersion(idHref[1]) + if ver == nil { + return nil + } + return &IDFilter{ + Id: idHref[0], + Version: ver, + } +} + +func IDFilterFromString(filter []string) []*IDFilter { + if len(filter) == 0 { + return nil + } + ret := make([]*IDFilter, 0, len(filter)) + for _, s := range filter { + f := idFilterFromString(s) + if f == nil { + continue + } + ret = append(ret, f) + } + return ret +} + +func (r *GetConditionsRequest) ConvertHTTPIDFilter() []*IDFilter { + return IDFilterFromString(r.GetHttpIdFilter()) +} + +func (r *GetConfigurationsRequest) ConvertHTTPIDFilter() []*IDFilter { + return IDFilterFromString(r.GetHttpIdFilter()) +} + +func (r *GetAppliedConfigurationsRequest) ConvertHTTPConfigurationIdFilter() []*IDFilter { + return IDFilterFromString(r.GetHttpConfigurationIdFilter()) +} + +func (r *GetAppliedConfigurationsRequest) ConvertHTTPConditionIdFilter() []*IDFilter { + return IDFilterFromString(r.GetHttpConditionIdFilter()) +} + +func (r *DeleteConfigurationsRequest) ConvertHTTPIDFilter() []*IDFilter { + return IDFilterFromString(r.GetHttpIdFilter()) +} + +func (r *DeleteConditionsRequest) ConvertHTTPIDFilter() []*IDFilter { + return IDFilterFromString(r.GetHttpIdFilter()) +} diff --git a/snippet-service/pb/idfilter_test.go b/snippet-service/pb/idfilter_test.go new file mode 100644 index 000000000..7eedc1401 --- /dev/null +++ b/snippet-service/pb/idfilter_test.go @@ -0,0 +1,157 @@ +package pb_test + +import ( + "testing" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/stretchr/testify/require" +) + +func TestNormalizeIDFilter(t *testing.T) { + tests := []struct { + name string + filter []*pb.IDFilter + want []*pb.IDFilter + }{ + { + name: "nil", + filter: nil, + want: nil, + }, + { + // if the query contains an IDFilter with an empty ID and All set to true, then all other filters are ignored + name: "all", + filter: []*pb.IDFilter{ + {Id: "id1", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id2", Version: &pb.IDFilter_Value{Value: 1}}, + {Version: &pb.IDFilter_All{All: true}}, + }, + want: nil, + }, + { + // if the query contains an IDFilter with All set to true then other filters for the ID are ignored + name: "remove non-All", + filter: []*pb.IDFilter{ + {Id: "id1", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id1", Version: &pb.IDFilter_Value{Value: 1}}, + {Id: "id1", Version: &pb.IDFilter_All{All: true}}, + }, + want: []*pb.IDFilter{ + {Id: "id1", Version: &pb.IDFilter_All{All: true}}, + }, + }, + { + name: "remove duplicates", + filter: []*pb.IDFilter{ + {Id: "id1", Version: &pb.IDFilter_All{All: true}}, + {Id: "id1", Version: &pb.IDFilter_All{All: true}}, + {Id: "id2", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id2", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id2", Version: &pb.IDFilter_Value{Value: 42}}, + {Id: "id2", Version: &pb.IDFilter_Value{Value: 42}}, + {Id: "id2", Version: &pb.IDFilter_Value{Value: 1}}, + {Id: "id2", Version: &pb.IDFilter_Value{Value: 1}}, + {Id: "id2", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id2", Version: &pb.IDFilter_Value{Value: 42}}, + }, + want: []*pb.IDFilter{ + {Id: "id1", Version: &pb.IDFilter_All{All: true}}, + {Id: "id2", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id2", Version: &pb.IDFilter_Value{Value: 1}}, + {Id: "id2", Version: &pb.IDFilter_Value{Value: 42}}, + }, + }, + { + name: "normalize", + filter: []*pb.IDFilter{ + {Id: "id3", Version: &pb.IDFilter_Value{Value: 3}}, + {Id: "id1", Version: &pb.IDFilter_Value{Value: 0}}, + {Id: "id3", Version: &pb.IDFilter_Value{Value: 3}}, + {Id: "id2", Version: &pb.IDFilter_All{All: true}}, + {Id: "id3", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id1", Version: &pb.IDFilter_Value{Value: 1}}, + {Id: "id3", Version: &pb.IDFilter_Value{Value: 1}}, + {Id: "id2", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id3", Version: &pb.IDFilter_Value{Value: 2}}, + {Id: "id2", Version: &pb.IDFilter_Value{Value: 42}}, + {Id: "id1", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id3", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id1", Version: &pb.IDFilter_Latest{Latest: true}}, + }, + want: []*pb.IDFilter{ + {Id: "id1", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id1", Version: &pb.IDFilter_Value{Value: 0}}, + {Id: "id1", Version: &pb.IDFilter_Value{Value: 1}}, + {Id: "id2", Version: &pb.IDFilter_All{All: true}}, + {Id: "id3", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id3", Version: &pb.IDFilter_Value{Value: 1}}, + {Id: "id3", Version: &pb.IDFilter_Value{Value: 2}}, + {Id: "id3", Version: &pb.IDFilter_Value{Value: 3}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.EqualValues(t, tt.want, pb.NormalizeIdFilter(tt.filter)) + }) + } +} + +func TestIDFilterFromString(t *testing.T) { + tests := []struct { + name string + filter []string + want []*pb.IDFilter + }{ + { + name: "nil", + filter: nil, + want: nil, + }, + { + name: "empty", + filter: []string{""}, + want: []*pb.IDFilter{}, + }, + { + name: "all", + filter: []string{"/id1", "/id2/", "id3/", "/id4/all"}, + want: []*pb.IDFilter{ + {Id: "id1", Version: &pb.IDFilter_All{All: true}}, + {Id: "id2", Version: &pb.IDFilter_All{All: true}}, + {Id: "id3", Version: &pb.IDFilter_All{All: true}}, + {Id: "id4", Version: &pb.IDFilter_All{All: true}}, + }, + }, + { + name: "latest", + filter: []string{"/id1/latest", "id2/latest"}, + want: []*pb.IDFilter{ + {Id: "id1", Version: &pb.IDFilter_Latest{Latest: true}}, + {Id: "id2", Version: &pb.IDFilter_Latest{Latest: true}}, + }, + }, + { + name: "value", + filter: []string{"/id1/0", "id2/1", "id3/2", "id4/42"}, + want: []*pb.IDFilter{ + {Id: "id1", Version: &pb.IDFilter_Value{Value: 0}}, + {Id: "id2", Version: &pb.IDFilter_Value{Value: 1}}, + {Id: "id3", Version: &pb.IDFilter_Value{Value: 2}}, + {Id: "id4", Version: &pb.IDFilter_Value{Value: 42}}, + }, + }, + { + name: "invalid", + filter: []string{"id1/fail", "/id2/fail"}, + want: []*pb.IDFilter{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.EqualValues(t, tt.want, pb.IDFilterFromString(tt.filter)) + }) + } +} diff --git a/snippet-service/pb/service.pb.go b/snippet-service/pb/service.pb.go new file mode 100644 index 000000000..7f23b6748 --- /dev/null +++ b/snippet-service/pb/service.pb.go @@ -0,0 +1,2091 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.1 +// protoc v5.26.1 +// source: snippet-service/pb/service.proto + +package pb + +import ( + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" + commands "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + events "github.com/plgd-dev/hub/v2/resource-aggregate/events" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AppliedConfiguration_Resource_Status int32 + +const ( + AppliedConfiguration_Resource_UNSPECIFIED AppliedConfiguration_Resource_Status = 0 + AppliedConfiguration_Resource_QUEUED AppliedConfiguration_Resource_Status = 1 + AppliedConfiguration_Resource_PENDING AppliedConfiguration_Resource_Status = 2 + AppliedConfiguration_Resource_DONE AppliedConfiguration_Resource_Status = 3 // If done look to resource_updated if update resource failed for resource aggregate. + AppliedConfiguration_Resource_TIMEOUT AppliedConfiguration_Resource_Status = 4 +) + +// Enum value maps for AppliedConfiguration_Resource_Status. +var ( + AppliedConfiguration_Resource_Status_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "QUEUED", + 2: "PENDING", + 3: "DONE", + 4: "TIMEOUT", + } + AppliedConfiguration_Resource_Status_value = map[string]int32{ + "UNSPECIFIED": 0, + "QUEUED": 1, + "PENDING": 2, + "DONE": 3, + "TIMEOUT": 4, + } +) + +func (x AppliedConfiguration_Resource_Status) Enum() *AppliedConfiguration_Resource_Status { + p := new(AppliedConfiguration_Resource_Status) + *p = x + return p +} + +func (x AppliedConfiguration_Resource_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AppliedConfiguration_Resource_Status) Descriptor() protoreflect.EnumDescriptor { + return file_snippet_service_pb_service_proto_enumTypes[0].Descriptor() +} + +func (AppliedConfiguration_Resource_Status) Type() protoreflect.EnumType { + return &file_snippet_service_pb_service_proto_enumTypes[0] +} + +func (x AppliedConfiguration_Resource_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AppliedConfiguration_Resource_Status.Descriptor instead. +func (AppliedConfiguration_Resource_Status) EnumDescriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{9, 0, 0} +} + +type IDFilter struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Types that are assignable to Version: + // + // *IDFilter_Value + // *IDFilter_All + // *IDFilter_Latest + Version isIDFilter_Version `protobuf_oneof:"version"` +} + +func (x *IDFilter) Reset() { + *x = IDFilter{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IDFilter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IDFilter) ProtoMessage() {} + +func (x *IDFilter) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_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 IDFilter.ProtoReflect.Descriptor instead. +func (*IDFilter) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{0} +} + +func (x *IDFilter) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (m *IDFilter) GetVersion() isIDFilter_Version { + if m != nil { + return m.Version + } + return nil +} + +func (x *IDFilter) GetValue() uint64 { + if x, ok := x.GetVersion().(*IDFilter_Value); ok { + return x.Value + } + return 0 +} + +func (x *IDFilter) GetAll() bool { + if x, ok := x.GetVersion().(*IDFilter_All); ok { + return x.All + } + return false +} + +func (x *IDFilter) GetLatest() bool { + if x, ok := x.GetVersion().(*IDFilter_Latest); ok { + return x.Latest + } + return false +} + +type isIDFilter_Version interface { + isIDFilter_Version() +} + +type IDFilter_Value struct { + Value uint64 `protobuf:"varint,2,opt,name=value,proto3,oneof"` +} + +type IDFilter_All struct { + All bool `protobuf:"varint,3,opt,name=all,proto3,oneof"` +} + +type IDFilter_Latest struct { + Latest bool `protobuf:"varint,4,opt,name=latest,proto3,oneof"` +} + +func (*IDFilter_Value) isIDFilter_Version() {} + +func (*IDFilter_All) isIDFilter_Version() {} + +func (*IDFilter_Latest) isIDFilter_Version() {} + +type Condition struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Condition ID + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Condition version + Version uint64 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + // User-friendly condition name + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // Condition is enabled/disabled + Enabled bool `protobuf:"varint,4,opt,name=enabled,proto3" json:"enabled,omitempty"` + // ID of the configuration to be applied when the condition is satisfied + ConfigurationId string `protobuf:"bytes,5,opt,name=configuration_id,json=configurationId,proto3" json:"configuration_id,omitempty"` + // list of device IDs to which the condition applies + DeviceIdFilter []string `protobuf:"bytes,6,rep,name=device_id_filter,json=deviceIdFilter,proto3" json:"device_id_filter,omitempty"` + ResourceTypeFilter []string `protobuf:"bytes,7,rep,name=resource_type_filter,json=resourceTypeFilter,proto3" json:"resource_type_filter,omitempty"` + // list of resource hrefs to which the condition applies + ResourceHrefFilter []string `protobuf:"bytes,8,rep,name=resource_href_filter,json=resourceHrefFilter,proto3" json:"resource_href_filter,omitempty"` + JqExpressionFilter string `protobuf:"bytes,9,opt,name=jq_expression_filter,json=jqExpressionFilter,proto3" json:"jq_expression_filter,omitempty"` + // Token used to update resources in the configuration + ApiAccessToken string `protobuf:"bytes,10,opt,name=api_access_token,json=apiAccessToken,proto3" json:"api_access_token,omitempty"` + // Condition owner + Owner string `protobuf:"bytes,11,opt,name=owner,proto3" json:"owner,omitempty"` + // Unix timestamp in ns when the condition has been created/updated + Timestamp int64 `protobuf:"varint,12,opt,name=timestamp,proto3" json:"timestamp,omitempty"` +} + +func (x *Condition) Reset() { + *x = Condition{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Condition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Condition) ProtoMessage() {} + +func (x *Condition) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_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 Condition.ProtoReflect.Descriptor instead. +func (*Condition) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{1} +} + +func (x *Condition) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Condition) GetVersion() uint64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Condition) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Condition) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *Condition) GetConfigurationId() string { + if x != nil { + return x.ConfigurationId + } + return "" +} + +func (x *Condition) GetDeviceIdFilter() []string { + if x != nil { + return x.DeviceIdFilter + } + return nil +} + +func (x *Condition) GetResourceTypeFilter() []string { + if x != nil { + return x.ResourceTypeFilter + } + return nil +} + +func (x *Condition) GetResourceHrefFilter() []string { + if x != nil { + return x.ResourceHrefFilter + } + return nil +} + +func (x *Condition) GetJqExpressionFilter() string { + if x != nil { + return x.JqExpressionFilter + } + return "" +} + +func (x *Condition) GetApiAccessToken() string { + if x != nil { + return x.ApiAccessToken + } + return "" +} + +func (x *Condition) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *Condition) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +type GetConditionsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IdFilter []*IDFilter `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + // returns latest conditions for given configurationId + ConfigurationIdFilter []string `protobuf:"bytes,2,rep,name=configuration_id_filter,json=configurationIdFilter,proto3" json:"configuration_id_filter,omitempty"` + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + // + // Deprecated: Marked as deprecated in snippet-service/pb/service.proto. + HttpIdFilter []string `protobuf:"bytes,3,rep,name=http_id_filter,json=httpIdFilter,proto3" json:"http_id_filter,omitempty"` +} + +func (x *GetConditionsRequest) Reset() { + *x = GetConditionsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetConditionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConditionsRequest) ProtoMessage() {} + +func (x *GetConditionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[2] + 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 GetConditionsRequest.ProtoReflect.Descriptor instead. +func (*GetConditionsRequest) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{2} +} + +func (x *GetConditionsRequest) GetIdFilter() []*IDFilter { + if x != nil { + return x.IdFilter + } + return nil +} + +func (x *GetConditionsRequest) GetConfigurationIdFilter() []string { + if x != nil { + return x.ConfigurationIdFilter + } + return nil +} + +// Deprecated: Marked as deprecated in snippet-service/pb/service.proto. +func (x *GetConditionsRequest) GetHttpIdFilter() []string { + if x != nil { + return x.HttpIdFilter + } + return nil +} + +type DeleteConditionsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IdFilter []*IDFilter `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + // + // Deprecated: Marked as deprecated in snippet-service/pb/service.proto. + HttpIdFilter []string `protobuf:"bytes,2,rep,name=http_id_filter,json=httpIdFilter,proto3" json:"http_id_filter,omitempty"` +} + +func (x *DeleteConditionsRequest) Reset() { + *x = DeleteConditionsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteConditionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteConditionsRequest) ProtoMessage() {} + +func (x *DeleteConditionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[3] + 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 DeleteConditionsRequest.ProtoReflect.Descriptor instead. +func (*DeleteConditionsRequest) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{3} +} + +func (x *DeleteConditionsRequest) GetIdFilter() []*IDFilter { + if x != nil { + return x.IdFilter + } + return nil +} + +// Deprecated: Marked as deprecated in snippet-service/pb/service.proto. +func (x *DeleteConditionsRequest) GetHttpIdFilter() []string { + if x != nil { + return x.HttpIdFilter + } + return nil +} + +type DeleteConditionsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *DeleteConditionsResponse) Reset() { + *x = DeleteConditionsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteConditionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteConditionsResponse) ProtoMessage() {} + +func (x *DeleteConditionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[4] + 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 DeleteConditionsResponse.ProtoReflect.Descriptor instead. +func (*DeleteConditionsResponse) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{4} +} + +func (x *DeleteConditionsResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type Configuration struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Configuration ID + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Configuration version + Version uint64 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + // User-friendly configuration name + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // List of resource updates to be applied + Resources []*Configuration_Resource `protobuf:"bytes,4,rep,name=resources,proto3" json:"resources,omitempty"` + // Configuration owner + Owner string `protobuf:"bytes,5,opt,name=owner,proto3" json:"owner,omitempty"` + // Unix timestamp in ns when the configuration has been created/updated + Timestamp int64 `protobuf:"varint,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"` +} + +func (x *Configuration) Reset() { + *x = Configuration{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Configuration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Configuration) ProtoMessage() {} + +func (x *Configuration) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[5] + 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 Configuration.ProtoReflect.Descriptor instead. +func (*Configuration) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{5} +} + +func (x *Configuration) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Configuration) GetVersion() uint64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Configuration) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Configuration) GetResources() []*Configuration_Resource { + if x != nil { + return x.Resources + } + return nil +} + +func (x *Configuration) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *Configuration) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +type GetConfigurationsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IdFilter []*IDFilter `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + // + // Deprecated: Marked as deprecated in snippet-service/pb/service.proto. + HttpIdFilter []string `protobuf:"bytes,2,rep,name=http_id_filter,json=httpIdFilter,proto3" json:"http_id_filter,omitempty"` +} + +func (x *GetConfigurationsRequest) Reset() { + *x = GetConfigurationsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetConfigurationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetConfigurationsRequest) ProtoMessage() {} + +func (x *GetConfigurationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[6] + 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 GetConfigurationsRequest.ProtoReflect.Descriptor instead. +func (*GetConfigurationsRequest) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{6} +} + +func (x *GetConfigurationsRequest) GetIdFilter() []*IDFilter { + if x != nil { + return x.IdFilter + } + return nil +} + +// Deprecated: Marked as deprecated in snippet-service/pb/service.proto. +func (x *GetConfigurationsRequest) GetHttpIdFilter() []string { + if x != nil { + return x.HttpIdFilter + } + return nil +} + +type DeleteConfigurationsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IdFilter []*IDFilter `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + // + // Deprecated: Marked as deprecated in snippet-service/pb/service.proto. + HttpIdFilter []string `protobuf:"bytes,2,rep,name=http_id_filter,json=httpIdFilter,proto3" json:"http_id_filter,omitempty"` +} + +func (x *DeleteConfigurationsRequest) Reset() { + *x = DeleteConfigurationsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteConfigurationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteConfigurationsRequest) ProtoMessage() {} + +func (x *DeleteConfigurationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[7] + 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 DeleteConfigurationsRequest.ProtoReflect.Descriptor instead. +func (*DeleteConfigurationsRequest) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{7} +} + +func (x *DeleteConfigurationsRequest) GetIdFilter() []*IDFilter { + if x != nil { + return x.IdFilter + } + return nil +} + +// Deprecated: Marked as deprecated in snippet-service/pb/service.proto. +func (x *DeleteConfigurationsRequest) GetHttpIdFilter() []string { + if x != nil { + return x.HttpIdFilter + } + return nil +} + +type DeleteConfigurationsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *DeleteConfigurationsResponse) Reset() { + *x = DeleteConfigurationsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteConfigurationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteConfigurationsResponse) ProtoMessage() {} + +func (x *DeleteConfigurationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[8] + 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 DeleteConfigurationsResponse.ProtoReflect.Descriptor instead. +func (*DeleteConfigurationsResponse) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{8} +} + +func (x *DeleteConfigurationsResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type AppliedConfiguration struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + DeviceId string `protobuf:"bytes,2,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty" bson:"deviceId"` + ConfigurationId *AppliedConfiguration_LinkedTo `protobuf:"bytes,3,opt,name=configuration_id,json=configurationId,proto3" json:"configuration_id,omitempty" bson:"configurationId"` + // Types that are assignable to ExecutedBy: + // + // *AppliedConfiguration_OnDemand + // *AppliedConfiguration_ConditionId + ExecutedBy isAppliedConfiguration_ExecutedBy `protobuf_oneof:"executed_by"` + Resources []*AppliedConfiguration_Resource `protobuf:"bytes,6,rep,name=resources,proto3" json:"resources,omitempty"` + Owner string `protobuf:"bytes,7,opt,name=owner,proto3" json:"owner,omitempty"` + // Unix timestamp in ns when the applied device configuration has been created/updated + Timestamp int64 `protobuf:"varint,8,opt,name=timestamp,proto3" json:"timestamp,omitempty"` +} + +func (x *AppliedConfiguration) Reset() { + *x = AppliedConfiguration{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AppliedConfiguration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppliedConfiguration) ProtoMessage() {} + +func (x *AppliedConfiguration) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[9] + 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 AppliedConfiguration.ProtoReflect.Descriptor instead. +func (*AppliedConfiguration) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{9} +} + +func (x *AppliedConfiguration) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *AppliedConfiguration) GetDeviceId() string { + if x != nil { + return x.DeviceId + } + return "" +} + +func (x *AppliedConfiguration) GetConfigurationId() *AppliedConfiguration_LinkedTo { + if x != nil { + return x.ConfigurationId + } + return nil +} + +func (m *AppliedConfiguration) GetExecutedBy() isAppliedConfiguration_ExecutedBy { + if m != nil { + return m.ExecutedBy + } + return nil +} + +func (x *AppliedConfiguration) GetOnDemand() bool { + if x, ok := x.GetExecutedBy().(*AppliedConfiguration_OnDemand); ok { + return x.OnDemand + } + return false +} + +func (x *AppliedConfiguration) GetConditionId() *AppliedConfiguration_LinkedTo { + if x, ok := x.GetExecutedBy().(*AppliedConfiguration_ConditionId); ok { + return x.ConditionId + } + return nil +} + +func (x *AppliedConfiguration) GetResources() []*AppliedConfiguration_Resource { + if x != nil { + return x.Resources + } + return nil +} + +func (x *AppliedConfiguration) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *AppliedConfiguration) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +type isAppliedConfiguration_ExecutedBy interface { + isAppliedConfiguration_ExecutedBy() +} + +type AppliedConfiguration_OnDemand struct { + OnDemand bool `protobuf:"varint,4,opt,name=on_demand,json=onDemand,proto3,oneof"` +} + +type AppliedConfiguration_ConditionId struct { + ConditionId *AppliedConfiguration_LinkedTo `protobuf:"bytes,5,opt,name=condition_id,json=conditionId,proto3,oneof" bson:"conditionId"` +} + +func (*AppliedConfiguration_OnDemand) isAppliedConfiguration_ExecutedBy() {} + +func (*AppliedConfiguration_ConditionId) isAppliedConfiguration_ExecutedBy() {} + +type InvokeConfigurationRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ConfigurationId string `protobuf:"bytes,1,opt,name=configuration_id,json=configurationId,proto3" json:"configuration_id,omitempty"` // applies latest configuration + DeviceId string `protobuf:"bytes,2,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"` + Force bool `protobuf:"varint,3,opt,name=force,proto3" json:"force,omitempty"` // force update even if the configuration has already been applied to device + CorrelationId string `protobuf:"bytes,4,opt,name=correlation_id,json=correlationId,proto3" json:"correlation_id,omitempty"` // propagated down to the resource update command +} + +func (x *InvokeConfigurationRequest) Reset() { + *x = InvokeConfigurationRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InvokeConfigurationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvokeConfigurationRequest) ProtoMessage() {} + +func (x *InvokeConfigurationRequest) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[10] + 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 InvokeConfigurationRequest.ProtoReflect.Descriptor instead. +func (*InvokeConfigurationRequest) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{10} +} + +func (x *InvokeConfigurationRequest) GetConfigurationId() string { + if x != nil { + return x.ConfigurationId + } + return "" +} + +func (x *InvokeConfigurationRequest) GetDeviceId() string { + if x != nil { + return x.DeviceId + } + return "" +} + +func (x *InvokeConfigurationRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + +func (x *InvokeConfigurationRequest) GetCorrelationId() string { + if x != nil { + return x.CorrelationId + } + return "" +} + +type InvokeConfigurationResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AppliedConfigurationId string `protobuf:"bytes,1,opt,name=applied_configuration_id,json=appliedConfigurationId,proto3" json:"applied_configuration_id,omitempty"` +} + +func (x *InvokeConfigurationResponse) Reset() { + *x = InvokeConfigurationResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InvokeConfigurationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvokeConfigurationResponse) ProtoMessage() {} + +func (x *InvokeConfigurationResponse) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[11] + 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 InvokeConfigurationResponse.ProtoReflect.Descriptor instead. +func (*InvokeConfigurationResponse) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{11} +} + +func (x *InvokeConfigurationResponse) GetAppliedConfigurationId() string { + if x != nil { + return x.AppliedConfigurationId + } + return "" +} + +type GetAppliedConfigurationsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IdFilter []string `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + DeviceIdFilter []string `protobuf:"bytes,2,rep,name=device_id_filter,json=deviceIdFilter,proto3" json:"device_id_filter,omitempty"` + ConfigurationIdFilter []*IDFilter `protobuf:"bytes,3,rep,name=configuration_id_filter,json=configurationIdFilter,proto3" json:"configuration_id_filter,omitempty"` + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + // + // Deprecated: Marked as deprecated in snippet-service/pb/service.proto. + HttpConfigurationIdFilter []string `protobuf:"bytes,4,rep,name=http_configuration_id_filter,json=httpConfigurationIdFilter,proto3" json:"http_configuration_id_filter,omitempty"` + ConditionIdFilter []*IDFilter `protobuf:"bytes,5,rep,name=condition_id_filter,json=conditionIdFilter,proto3" json:"condition_id_filter,omitempty"` + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + // + // Deprecated: Marked as deprecated in snippet-service/pb/service.proto. + HttpConditionIdFilter []string `protobuf:"bytes,6,rep,name=http_condition_id_filter,json=httpConditionIdFilter,proto3" json:"http_condition_id_filter,omitempty"` +} + +func (x *GetAppliedConfigurationsRequest) Reset() { + *x = GetAppliedConfigurationsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetAppliedConfigurationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAppliedConfigurationsRequest) ProtoMessage() {} + +func (x *GetAppliedConfigurationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[12] + 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 GetAppliedConfigurationsRequest.ProtoReflect.Descriptor instead. +func (*GetAppliedConfigurationsRequest) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{12} +} + +func (x *GetAppliedConfigurationsRequest) GetIdFilter() []string { + if x != nil { + return x.IdFilter + } + return nil +} + +func (x *GetAppliedConfigurationsRequest) GetDeviceIdFilter() []string { + if x != nil { + return x.DeviceIdFilter + } + return nil +} + +func (x *GetAppliedConfigurationsRequest) GetConfigurationIdFilter() []*IDFilter { + if x != nil { + return x.ConfigurationIdFilter + } + return nil +} + +// Deprecated: Marked as deprecated in snippet-service/pb/service.proto. +func (x *GetAppliedConfigurationsRequest) GetHttpConfigurationIdFilter() []string { + if x != nil { + return x.HttpConfigurationIdFilter + } + return nil +} + +func (x *GetAppliedConfigurationsRequest) GetConditionIdFilter() []*IDFilter { + if x != nil { + return x.ConditionIdFilter + } + return nil +} + +// Deprecated: Marked as deprecated in snippet-service/pb/service.proto. +func (x *GetAppliedConfigurationsRequest) GetHttpConditionIdFilter() []string { + if x != nil { + return x.HttpConditionIdFilter + } + return nil +} + +type DeleteAppliedConfigurationsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IdFilter []string `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` +} + +func (x *DeleteAppliedConfigurationsRequest) Reset() { + *x = DeleteAppliedConfigurationsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteAppliedConfigurationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAppliedConfigurationsRequest) ProtoMessage() {} + +func (x *DeleteAppliedConfigurationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[13] + 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 DeleteAppliedConfigurationsRequest.ProtoReflect.Descriptor instead. +func (*DeleteAppliedConfigurationsRequest) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{13} +} + +func (x *DeleteAppliedConfigurationsRequest) GetIdFilter() []string { + if x != nil { + return x.IdFilter + } + return nil +} + +type DeleteAppliedConfigurationsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` +} + +func (x *DeleteAppliedConfigurationsResponse) Reset() { + *x = DeleteAppliedConfigurationsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteAppliedConfigurationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAppliedConfigurationsResponse) ProtoMessage() {} + +func (x *DeleteAppliedConfigurationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[14] + 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 DeleteAppliedConfigurationsResponse.ProtoReflect.Descriptor instead. +func (*DeleteAppliedConfigurationsResponse) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{14} +} + +func (x *DeleteAppliedConfigurationsResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +type Configuration_Resource struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // href of the resource + Href string `protobuf:"bytes,1,opt,name=href,proto3" json:"href,omitempty"` + // content update of the resource + Content *commands.Content `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + // optional update command time to live, 0 is infinite + TimeToLive int64 `protobuf:"varint,3,opt,name=time_to_live,json=timeToLive,proto3" json:"time_to_live,omitempty"` +} + +func (x *Configuration_Resource) Reset() { + *x = Configuration_Resource{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Configuration_Resource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Configuration_Resource) ProtoMessage() {} + +func (x *Configuration_Resource) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[15] + 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 Configuration_Resource.ProtoReflect.Descriptor instead. +func (*Configuration_Resource) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{5, 0} +} + +func (x *Configuration_Resource) GetHref() string { + if x != nil { + return x.Href + } + return "" +} + +func (x *Configuration_Resource) GetContent() *commands.Content { + if x != nil { + return x.Content + } + return nil +} + +func (x *Configuration_Resource) GetTimeToLive() int64 { + if x != nil { + return x.TimeToLive + } + return 0 +} + +type AppliedConfiguration_Resource struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Href string `protobuf:"bytes,1,opt,name=href,proto3" json:"href,omitempty"` + // Reused from invoke command or generated. Can be used to retrieve corresponding pending command. + CorrelationId string `protobuf:"bytes,2,opt,name=correlation_id,json=correlationId,proto3" json:"correlation_id,omitempty" bson:"correlationId"` + Status AppliedConfiguration_Resource_Status `protobuf:"varint,3,opt,name=status,proto3,enum=snippetservice.pb.AppliedConfiguration_Resource_Status" json:"status,omitempty"` + ResourceUpdated *events.ResourceUpdated `protobuf:"bytes,4,opt,name=resource_updated,json=resourceUpdated,proto3" json:"resource_updated,omitempty" bson:"resourceUpdated,omitempty"` + // Unix nanoseconds timestamp for resource in PENDING status, until which the pending update is valid + ValidUntil int64 `protobuf:"varint,5,opt,name=valid_until,json=validUntil,proto3" json:"valid_until,omitempty" bson:"validUntil,omitempty"` +} + +func (x *AppliedConfiguration_Resource) Reset() { + *x = AppliedConfiguration_Resource{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AppliedConfiguration_Resource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppliedConfiguration_Resource) ProtoMessage() {} + +func (x *AppliedConfiguration_Resource) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[16] + 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 AppliedConfiguration_Resource.ProtoReflect.Descriptor instead. +func (*AppliedConfiguration_Resource) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{9, 0} +} + +func (x *AppliedConfiguration_Resource) GetHref() string { + if x != nil { + return x.Href + } + return "" +} + +func (x *AppliedConfiguration_Resource) GetCorrelationId() string { + if x != nil { + return x.CorrelationId + } + return "" +} + +func (x *AppliedConfiguration_Resource) GetStatus() AppliedConfiguration_Resource_Status { + if x != nil { + return x.Status + } + return AppliedConfiguration_Resource_UNSPECIFIED +} + +func (x *AppliedConfiguration_Resource) GetResourceUpdated() *events.ResourceUpdated { + if x != nil { + return x.ResourceUpdated + } + return nil +} + +func (x *AppliedConfiguration_Resource) GetValidUntil() int64 { + if x != nil { + return x.ValidUntil + } + return 0 +} + +type AppliedConfiguration_LinkedTo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Version uint64 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` +} + +func (x *AppliedConfiguration_LinkedTo) Reset() { + *x = AppliedConfiguration_LinkedTo{} + if protoimpl.UnsafeEnabled { + mi := &file_snippet_service_pb_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AppliedConfiguration_LinkedTo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppliedConfiguration_LinkedTo) ProtoMessage() {} + +func (x *AppliedConfiguration_LinkedTo) ProtoReflect() protoreflect.Message { + mi := &file_snippet_service_pb_service_proto_msgTypes[17] + 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 AppliedConfiguration_LinkedTo.ProtoReflect.Descriptor instead. +func (*AppliedConfiguration_LinkedTo) Descriptor() ([]byte, []int) { + return file_snippet_service_pb_service_proto_rawDescGZIP(), []int{9, 1} +} + +func (x *AppliedConfiguration_LinkedTo) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *AppliedConfiguration_LinkedTo) GetVersion() uint64 { + if x != nil { + return x.Version + } + return 0 +} + +var File_snippet_service_pb_service_proto protoreflect.FileDescriptor + +var file_snippet_service_pb_service_proto_rawDesc = []byte{ + 0x0a, 0x20, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x2f, 0x70, 0x62, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x11, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x2e, 0x70, 0x62, 0x1a, 0x25, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2d, + 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2f, 0x70, 0x62, 0x2f, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x22, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2d, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, + 0x2f, 0x70, 0x62, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, + 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e, 0x61, + 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, + 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6b, + 0x0a, 0x08, 0x49, 0x44, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x12, 0x12, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x48, + 0x00, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x12, 0x18, 0x0a, 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, + 0x42, 0x09, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xac, 0x03, 0x0a, 0x09, + 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, + 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x07, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, + 0x70, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x30, 0x0a, 0x14, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x72, 0x65, 0x66, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x48, 0x72, 0x65, 0x66, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x30, 0x0a, 0x14, 0x6a, 0x71, + 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6a, 0x71, 0x45, 0x78, 0x70, 0x72, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, + 0x61, 0x70, 0x69, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x70, 0x69, 0x41, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0xb2, 0x01, 0x0a, 0x14, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x44, 0x46, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x36, 0x0a, + 0x17, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x15, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x46, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x0e, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x69, 0x64, + 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x42, 0x02, 0x18, + 0x01, 0x52, 0x0c, 0x68, 0x74, 0x74, 0x70, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, + 0x7d, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, + 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, + 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, + 0x62, 0x2e, 0x49, 0x44, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x0e, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x69, 0x64, 0x5f, + 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, + 0x52, 0x0c, 0x68, 0x74, 0x74, 0x70, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x34, + 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x22, 0xc5, 0x02, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x47, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, + 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x14, 0x0a, + 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, + 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x1a, 0x79, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x68, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x72, 0x65, + 0x66, 0x12, 0x37, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, + 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x69, + 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x6f, 0x4c, 0x69, 0x76, 0x65, 0x22, 0x7e, 0x0a, 0x18, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x73, 0x6e, + 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x49, 0x44, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x12, 0x28, 0x0a, 0x0e, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0c, + 0x68, 0x74, 0x74, 0x70, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x81, 0x01, 0x0a, + 0x1b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x09, + 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x44, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x08, 0x69, 0x64, + 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x0e, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x69, + 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x42, 0x02, + 0x18, 0x01, 0x52, 0x0c, 0x68, 0x74, 0x74, 0x70, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x22, 0x38, 0x0a, 0x1c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0xb6, 0x06, 0x0a, 0x14, 0x41, + 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, + 0x12, 0x5b, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x73, 0x6e, 0x69, + 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, + 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x6e, 0x6b, 0x65, 0x64, 0x54, 0x6f, 0x52, 0x0f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, + 0x09, 0x6f, 0x6e, 0x5f, 0x64, 0x65, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, + 0x48, 0x00, 0x52, 0x08, 0x6f, 0x6e, 0x44, 0x65, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x55, 0x0a, 0x0c, + 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x6e, 0x6b, + 0x65, 0x64, 0x54, 0x6f, 0x48, 0x00, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x12, 0x4e, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, + 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x1a, 0xd4, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x68, 0x72, 0x65, 0x66, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, + 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x4f, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x37, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x50, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x2e, 0x70, + 0x62, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, + 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, + 0x74, 0x69, 0x6c, 0x22, 0x49, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0f, 0x0a, + 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0a, + 0x0a, 0x06, 0x51, 0x55, 0x45, 0x55, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x45, + 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x4f, 0x4e, 0x45, 0x10, + 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x04, 0x1a, 0x34, + 0x0a, 0x08, 0x4c, 0x69, 0x6e, 0x6b, 0x65, 0x64, 0x54, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x0d, 0x0a, 0x0b, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x64, + 0x5f, 0x62, 0x79, 0x22, 0xa1, 0x01, 0x0a, 0x1a, 0x49, 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, + 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, + 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, + 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x57, 0x0a, 0x1b, 0x49, 0x6e, 0x76, 0x6f, 0x6b, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x18, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x65, + 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x65, + 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x22, 0x8c, 0x03, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x12, 0x28, 0x0a, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x53, 0x0a, 0x17, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x5f, + 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x73, + 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, + 0x2e, 0x49, 0x44, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x12, 0x43, 0x0a, 0x1c, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x19, 0x68, 0x74, 0x74, 0x70, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x46, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x4b, 0x0a, 0x13, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x44, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, + 0x11, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x12, 0x3b, 0x0a, 0x18, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x06, + 0x20, 0x03, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x15, 0x68, 0x74, 0x74, 0x70, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, + 0x41, 0x0a, 0x22, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x22, 0x3f, 0x0a, 0x23, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x70, 0x6c, + 0x69, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x32, 0x91, 0x0f, 0x0a, 0x0e, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x8b, 0x01, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x73, 0x6e, 0x69, + 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, + 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x1c, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, + 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3c, 0x92, 0x41, 0x0c, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x27, 0x3a, 0x01, 0x2a, + 0x22, 0x22, 0x2f, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x93, 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x39, 0x92, + 0x41, 0x0c, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x24, 0x12, 0x22, 0x2f, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x2d, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01, 0x12, 0xa6, 0x01, 0x0a, 0x10, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x2a, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x73, 0x6e, + 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x39, 0x92, 0x41, 0x0c, 0x0a, 0x0a, 0x43, + 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x2a, + 0x22, 0x2f, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x90, 0x01, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, + 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x1c, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x92, 0x41, 0x0c, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2c, 0x3a, 0x01, 0x2a, 0x1a, 0x27, 0x2f, + 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x9f, 0x01, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, + 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, + 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x1a, 0x20, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x22, 0x44, 0x92, 0x41, 0x10, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x3a, 0x01, 0x2a, + 0x22, 0x26, 0x2f, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0xa7, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, + 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, + 0x70, 0x62, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x73, 0x6e, + 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x92, + 0x41, 0x10, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x28, 0x12, 0x26, 0x2f, 0x73, 0x6e, 0x69, 0x70, 0x70, + 0x65, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, + 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x30, 0x01, 0x12, 0xba, 0x01, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x2e, 0x73, 0x6e, + 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x73, 0x6e, + 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x41, 0x92, 0x41, + 0x10, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x28, 0x2a, 0x26, 0x2f, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, + 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0xa4, 0x01, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, + 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x20, 0x2e, 0x73, 0x6e, 0x69, 0x70, + 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x49, 0x92, 0x41, 0x10, + 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x30, 0x3a, 0x01, 0x2a, 0x1a, 0x2b, 0x2f, 0x73, 0x6e, 0x69, 0x70, + 0x70, 0x65, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xcd, 0x01, 0x0a, 0x13, 0x49, 0x6e, 0x76, 0x6f, 0x6b, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2d, + 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, + 0x70, 0x62, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, + 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, + 0x62, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x57, 0x92, + 0x41, 0x10, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3e, 0x3a, 0x01, 0x2a, 0x22, 0x39, 0x2f, 0x73, 0x6e, + 0x69, 0x70, 0x70, 0x65, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0xc4, 0x01, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x41, 0x70, + 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x32, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x70, 0x70, 0x6c, 0x69, + 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, + 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x41, 0x70, 0x70, 0x6c, + 0x69, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0x49, 0x92, 0x41, 0x10, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x30, 0x12, 0x2e, 0x2f, 0x73, 0x6e, + 0x69, 0x70, 0x70, 0x65, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x30, 0x01, 0x12, 0xd7, 0x01, + 0x0a, 0x1b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x35, 0x2e, + 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, + 0x62, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, + 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x49, 0x92, 0x41, + 0x10, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x30, 0x2a, 0x2e, 0x2f, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, + 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, + 0x61, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x42, 0xd5, 0x02, 0x92, 0x41, 0x9f, 0x02, 0x12, 0xc7, + 0x01, 0x0a, 0x14, 0x50, 0x4c, 0x47, 0x44, 0x20, 0x52, 0x75, 0x6c, 0x65, 0x20, 0x45, 0x6e, 0x67, + 0x69, 0x6e, 0x65, 0x20, 0x41, 0x50, 0x49, 0x12, 0x27, 0x41, 0x50, 0x49, 0x20, 0x66, 0x6f, 0x72, + 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x72, 0x75, 0x6c, + 0x65, 0x20, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x20, 0x69, 0x6e, 0x20, 0x50, 0x4c, 0x47, 0x44, + 0x22, 0x3a, 0x0a, 0x08, 0x70, 0x6c, 0x67, 0x64, 0x2e, 0x64, 0x65, 0x76, 0x12, 0x1f, 0x68, 0x74, + 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x1a, 0x0d, 0x69, + 0x6e, 0x66, 0x6f, 0x40, 0x70, 0x6c, 0x67, 0x64, 0x2e, 0x64, 0x65, 0x76, 0x2a, 0x45, 0x0a, 0x12, + 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x20, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x20, 0x32, + 0x2e, 0x30, 0x12, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, + 0x68, 0x75, 0x62, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x4c, 0x49, 0x43, 0x45, + 0x4e, 0x53, 0x45, 0x32, 0x03, 0x31, 0x2e, 0x30, 0x2a, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, + 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x32, 0x15, 0x61, + 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x15, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6a, 0x73, 0x6f, 0x6e, 0x5a, 0x30, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, + 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, + 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_snippet_service_pb_service_proto_rawDescOnce sync.Once + file_snippet_service_pb_service_proto_rawDescData = file_snippet_service_pb_service_proto_rawDesc +) + +func file_snippet_service_pb_service_proto_rawDescGZIP() []byte { + file_snippet_service_pb_service_proto_rawDescOnce.Do(func() { + file_snippet_service_pb_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_snippet_service_pb_service_proto_rawDescData) + }) + return file_snippet_service_pb_service_proto_rawDescData +} + +var file_snippet_service_pb_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_snippet_service_pb_service_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_snippet_service_pb_service_proto_goTypes = []interface{}{ + (AppliedConfiguration_Resource_Status)(0), // 0: snippetservice.pb.AppliedConfiguration.Resource.Status + (*IDFilter)(nil), // 1: snippetservice.pb.IDFilter + (*Condition)(nil), // 2: snippetservice.pb.Condition + (*GetConditionsRequest)(nil), // 3: snippetservice.pb.GetConditionsRequest + (*DeleteConditionsRequest)(nil), // 4: snippetservice.pb.DeleteConditionsRequest + (*DeleteConditionsResponse)(nil), // 5: snippetservice.pb.DeleteConditionsResponse + (*Configuration)(nil), // 6: snippetservice.pb.Configuration + (*GetConfigurationsRequest)(nil), // 7: snippetservice.pb.GetConfigurationsRequest + (*DeleteConfigurationsRequest)(nil), // 8: snippetservice.pb.DeleteConfigurationsRequest + (*DeleteConfigurationsResponse)(nil), // 9: snippetservice.pb.DeleteConfigurationsResponse + (*AppliedConfiguration)(nil), // 10: snippetservice.pb.AppliedConfiguration + (*InvokeConfigurationRequest)(nil), // 11: snippetservice.pb.InvokeConfigurationRequest + (*InvokeConfigurationResponse)(nil), // 12: snippetservice.pb.InvokeConfigurationResponse + (*GetAppliedConfigurationsRequest)(nil), // 13: snippetservice.pb.GetAppliedConfigurationsRequest + (*DeleteAppliedConfigurationsRequest)(nil), // 14: snippetservice.pb.DeleteAppliedConfigurationsRequest + (*DeleteAppliedConfigurationsResponse)(nil), // 15: snippetservice.pb.DeleteAppliedConfigurationsResponse + (*Configuration_Resource)(nil), // 16: snippetservice.pb.Configuration.Resource + (*AppliedConfiguration_Resource)(nil), // 17: snippetservice.pb.AppliedConfiguration.Resource + (*AppliedConfiguration_LinkedTo)(nil), // 18: snippetservice.pb.AppliedConfiguration.LinkedTo + (*commands.Content)(nil), // 19: resourceaggregate.pb.Content + (*events.ResourceUpdated)(nil), // 20: resourceaggregate.pb.ResourceUpdated +} +var file_snippet_service_pb_service_proto_depIdxs = []int32{ + 1, // 0: snippetservice.pb.GetConditionsRequest.id_filter:type_name -> snippetservice.pb.IDFilter + 1, // 1: snippetservice.pb.DeleteConditionsRequest.id_filter:type_name -> snippetservice.pb.IDFilter + 16, // 2: snippetservice.pb.Configuration.resources:type_name -> snippetservice.pb.Configuration.Resource + 1, // 3: snippetservice.pb.GetConfigurationsRequest.id_filter:type_name -> snippetservice.pb.IDFilter + 1, // 4: snippetservice.pb.DeleteConfigurationsRequest.id_filter:type_name -> snippetservice.pb.IDFilter + 18, // 5: snippetservice.pb.AppliedConfiguration.configuration_id:type_name -> snippetservice.pb.AppliedConfiguration.LinkedTo + 18, // 6: snippetservice.pb.AppliedConfiguration.condition_id:type_name -> snippetservice.pb.AppliedConfiguration.LinkedTo + 17, // 7: snippetservice.pb.AppliedConfiguration.resources:type_name -> snippetservice.pb.AppliedConfiguration.Resource + 1, // 8: snippetservice.pb.GetAppliedConfigurationsRequest.configuration_id_filter:type_name -> snippetservice.pb.IDFilter + 1, // 9: snippetservice.pb.GetAppliedConfigurationsRequest.condition_id_filter:type_name -> snippetservice.pb.IDFilter + 19, // 10: snippetservice.pb.Configuration.Resource.content:type_name -> resourceaggregate.pb.Content + 0, // 11: snippetservice.pb.AppliedConfiguration.Resource.status:type_name -> snippetservice.pb.AppliedConfiguration.Resource.Status + 20, // 12: snippetservice.pb.AppliedConfiguration.Resource.resource_updated:type_name -> resourceaggregate.pb.ResourceUpdated + 2, // 13: snippetservice.pb.SnippetService.CreateCondition:input_type -> snippetservice.pb.Condition + 3, // 14: snippetservice.pb.SnippetService.GetConditions:input_type -> snippetservice.pb.GetConditionsRequest + 4, // 15: snippetservice.pb.SnippetService.DeleteConditions:input_type -> snippetservice.pb.DeleteConditionsRequest + 2, // 16: snippetservice.pb.SnippetService.UpdateCondition:input_type -> snippetservice.pb.Condition + 6, // 17: snippetservice.pb.SnippetService.CreateConfiguration:input_type -> snippetservice.pb.Configuration + 7, // 18: snippetservice.pb.SnippetService.GetConfigurations:input_type -> snippetservice.pb.GetConfigurationsRequest + 8, // 19: snippetservice.pb.SnippetService.DeleteConfigurations:input_type -> snippetservice.pb.DeleteConfigurationsRequest + 6, // 20: snippetservice.pb.SnippetService.UpdateConfiguration:input_type -> snippetservice.pb.Configuration + 11, // 21: snippetservice.pb.SnippetService.InvokeConfiguration:input_type -> snippetservice.pb.InvokeConfigurationRequest + 13, // 22: snippetservice.pb.SnippetService.GetAppliedConfigurations:input_type -> snippetservice.pb.GetAppliedConfigurationsRequest + 14, // 23: snippetservice.pb.SnippetService.DeleteAppliedConfigurations:input_type -> snippetservice.pb.DeleteAppliedConfigurationsRequest + 2, // 24: snippetservice.pb.SnippetService.CreateCondition:output_type -> snippetservice.pb.Condition + 2, // 25: snippetservice.pb.SnippetService.GetConditions:output_type -> snippetservice.pb.Condition + 5, // 26: snippetservice.pb.SnippetService.DeleteConditions:output_type -> snippetservice.pb.DeleteConditionsResponse + 2, // 27: snippetservice.pb.SnippetService.UpdateCondition:output_type -> snippetservice.pb.Condition + 6, // 28: snippetservice.pb.SnippetService.CreateConfiguration:output_type -> snippetservice.pb.Configuration + 6, // 29: snippetservice.pb.SnippetService.GetConfigurations:output_type -> snippetservice.pb.Configuration + 9, // 30: snippetservice.pb.SnippetService.DeleteConfigurations:output_type -> snippetservice.pb.DeleteConfigurationsResponse + 6, // 31: snippetservice.pb.SnippetService.UpdateConfiguration:output_type -> snippetservice.pb.Configuration + 12, // 32: snippetservice.pb.SnippetService.InvokeConfiguration:output_type -> snippetservice.pb.InvokeConfigurationResponse + 10, // 33: snippetservice.pb.SnippetService.GetAppliedConfigurations:output_type -> snippetservice.pb.AppliedConfiguration + 15, // 34: snippetservice.pb.SnippetService.DeleteAppliedConfigurations:output_type -> snippetservice.pb.DeleteAppliedConfigurationsResponse + 24, // [24:35] is the sub-list for method output_type + 13, // [13:24] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name +} + +func init() { file_snippet_service_pb_service_proto_init() } +func file_snippet_service_pb_service_proto_init() { + if File_snippet_service_pb_service_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_snippet_service_pb_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IDFilter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Condition); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetConditionsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteConditionsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteConditionsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Configuration); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetConfigurationsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteConfigurationsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteConfigurationsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AppliedConfiguration); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InvokeConfigurationRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InvokeConfigurationResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetAppliedConfigurationsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteAppliedConfigurationsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteAppliedConfigurationsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Configuration_Resource); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AppliedConfiguration_Resource); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_snippet_service_pb_service_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AppliedConfiguration_LinkedTo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_snippet_service_pb_service_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*IDFilter_Value)(nil), + (*IDFilter_All)(nil), + (*IDFilter_Latest)(nil), + } + file_snippet_service_pb_service_proto_msgTypes[9].OneofWrappers = []interface{}{ + (*AppliedConfiguration_OnDemand)(nil), + (*AppliedConfiguration_ConditionId)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_snippet_service_pb_service_proto_rawDesc, + NumEnums: 1, + NumMessages: 18, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_snippet_service_pb_service_proto_goTypes, + DependencyIndexes: file_snippet_service_pb_service_proto_depIdxs, + EnumInfos: file_snippet_service_pb_service_proto_enumTypes, + MessageInfos: file_snippet_service_pb_service_proto_msgTypes, + }.Build() + File_snippet_service_pb_service_proto = out.File + file_snippet_service_pb_service_proto_rawDesc = nil + file_snippet_service_pb_service_proto_goTypes = nil + file_snippet_service_pb_service_proto_depIdxs = nil +} diff --git a/snippet-service/pb/service.pb.gw.go b/snippet-service/pb/service.pb.gw.go new file mode 100644 index 000000000..191bb172d --- /dev/null +++ b/snippet-service/pb/service.pb.gw.go @@ -0,0 +1,1017 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: snippet-service/pb/service.proto + +/* +Package pb is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package pb + +import ( + "context" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray +var _ = metadata.Join + +func request_SnippetService_CreateCondition_0(ctx context.Context, marshaler runtime.Marshaler, client SnippetServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Condition + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.CreateCondition(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_SnippetService_CreateCondition_0(ctx context.Context, marshaler runtime.Marshaler, server SnippetServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Condition + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.CreateCondition(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_SnippetService_GetConditions_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_SnippetService_GetConditions_0(ctx context.Context, marshaler runtime.Marshaler, client SnippetServiceClient, req *http.Request, pathParams map[string]string) (SnippetService_GetConditionsClient, runtime.ServerMetadata, error) { + var protoReq GetConditionsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SnippetService_GetConditions_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.GetConditions(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +var ( + filter_SnippetService_DeleteConditions_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_SnippetService_DeleteConditions_0(ctx context.Context, marshaler runtime.Marshaler, client SnippetServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteConditionsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SnippetService_DeleteConditions_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.DeleteConditions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_SnippetService_DeleteConditions_0(ctx context.Context, marshaler runtime.Marshaler, server SnippetServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteConditionsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SnippetService_DeleteConditions_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.DeleteConditions(ctx, &protoReq) + return msg, metadata, err + +} + +func request_SnippetService_UpdateCondition_0(ctx context.Context, marshaler runtime.Marshaler, client SnippetServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Condition + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := client.UpdateCondition(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_SnippetService_UpdateCondition_0(ctx context.Context, marshaler runtime.Marshaler, server SnippetServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Condition + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := server.UpdateCondition(ctx, &protoReq) + return msg, metadata, err + +} + +func request_SnippetService_CreateConfiguration_0(ctx context.Context, marshaler runtime.Marshaler, client SnippetServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Configuration + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.CreateConfiguration(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_SnippetService_CreateConfiguration_0(ctx context.Context, marshaler runtime.Marshaler, server SnippetServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Configuration + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.CreateConfiguration(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_SnippetService_GetConfigurations_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_SnippetService_GetConfigurations_0(ctx context.Context, marshaler runtime.Marshaler, client SnippetServiceClient, req *http.Request, pathParams map[string]string) (SnippetService_GetConfigurationsClient, runtime.ServerMetadata, error) { + var protoReq GetConfigurationsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SnippetService_GetConfigurations_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.GetConfigurations(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +var ( + filter_SnippetService_DeleteConfigurations_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_SnippetService_DeleteConfigurations_0(ctx context.Context, marshaler runtime.Marshaler, client SnippetServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteConfigurationsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SnippetService_DeleteConfigurations_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.DeleteConfigurations(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_SnippetService_DeleteConfigurations_0(ctx context.Context, marshaler runtime.Marshaler, server SnippetServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteConfigurationsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SnippetService_DeleteConfigurations_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.DeleteConfigurations(ctx, &protoReq) + return msg, metadata, err + +} + +func request_SnippetService_UpdateConfiguration_0(ctx context.Context, marshaler runtime.Marshaler, client SnippetServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Configuration + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := client.UpdateConfiguration(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_SnippetService_UpdateConfiguration_0(ctx context.Context, marshaler runtime.Marshaler, server SnippetServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq Configuration + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := server.UpdateConfiguration(ctx, &protoReq) + return msg, metadata, err + +} + +func request_SnippetService_InvokeConfiguration_0(ctx context.Context, marshaler runtime.Marshaler, client SnippetServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq InvokeConfigurationRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["configuration_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "configuration_id") + } + + protoReq.ConfigurationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "configuration_id", err) + } + + msg, err := client.InvokeConfiguration(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_SnippetService_InvokeConfiguration_0(ctx context.Context, marshaler runtime.Marshaler, server SnippetServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq InvokeConfigurationRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["configuration_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "configuration_id") + } + + protoReq.ConfigurationId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "configuration_id", err) + } + + msg, err := server.InvokeConfiguration(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_SnippetService_GetAppliedConfigurations_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_SnippetService_GetAppliedConfigurations_0(ctx context.Context, marshaler runtime.Marshaler, client SnippetServiceClient, req *http.Request, pathParams map[string]string) (SnippetService_GetAppliedConfigurationsClient, runtime.ServerMetadata, error) { + var protoReq GetAppliedConfigurationsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SnippetService_GetAppliedConfigurations_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.GetAppliedConfigurations(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +var ( + filter_SnippetService_DeleteAppliedConfigurations_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_SnippetService_DeleteAppliedConfigurations_0(ctx context.Context, marshaler runtime.Marshaler, client SnippetServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteAppliedConfigurationsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SnippetService_DeleteAppliedConfigurations_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.DeleteAppliedConfigurations(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_SnippetService_DeleteAppliedConfigurations_0(ctx context.Context, marshaler runtime.Marshaler, server SnippetServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq DeleteAppliedConfigurationsRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SnippetService_DeleteAppliedConfigurations_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.DeleteAppliedConfigurations(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterSnippetServiceHandlerServer registers the http handlers for service SnippetService to "mux". +// UnaryRPC :call SnippetServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterSnippetServiceHandlerFromEndpoint instead. +func RegisterSnippetServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server SnippetServiceServer) error { + + mux.Handle("POST", pattern_SnippetService_CreateCondition_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/snippetservice.pb.SnippetService/CreateCondition", runtime.WithHTTPPathPattern("/snippet-service/api/v1/conditions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_SnippetService_CreateCondition_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_CreateCondition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_SnippetService_GetConditions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("DELETE", pattern_SnippetService_DeleteConditions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/snippetservice.pb.SnippetService/DeleteConditions", runtime.WithHTTPPathPattern("/snippet-service/api/v1/conditions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_SnippetService_DeleteConditions_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_DeleteConditions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PUT", pattern_SnippetService_UpdateCondition_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/snippetservice.pb.SnippetService/UpdateCondition", runtime.WithHTTPPathPattern("/snippet-service/api/v1/conditions/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_SnippetService_UpdateCondition_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_UpdateCondition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_SnippetService_CreateConfiguration_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/snippetservice.pb.SnippetService/CreateConfiguration", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_SnippetService_CreateConfiguration_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_CreateConfiguration_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_SnippetService_GetConfigurations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("DELETE", pattern_SnippetService_DeleteConfigurations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/snippetservice.pb.SnippetService/DeleteConfigurations", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_SnippetService_DeleteConfigurations_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_DeleteConfigurations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PUT", pattern_SnippetService_UpdateConfiguration_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/snippetservice.pb.SnippetService/UpdateConfiguration", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_SnippetService_UpdateConfiguration_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_UpdateConfiguration_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_SnippetService_InvokeConfiguration_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/snippetservice.pb.SnippetService/InvokeConfiguration", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations/{configuration_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_SnippetService_InvokeConfiguration_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_InvokeConfiguration_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_SnippetService_GetAppliedConfigurations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("DELETE", pattern_SnippetService_DeleteAppliedConfigurations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/snippetservice.pb.SnippetService/DeleteAppliedConfigurations", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations/applied")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_SnippetService_DeleteAppliedConfigurations_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_DeleteAppliedConfigurations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +// RegisterSnippetServiceHandlerFromEndpoint is same as RegisterSnippetServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterSnippetServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterSnippetServiceHandler(ctx, mux, conn) +} + +// RegisterSnippetServiceHandler registers the http handlers for service SnippetService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterSnippetServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterSnippetServiceHandlerClient(ctx, mux, NewSnippetServiceClient(conn)) +} + +// RegisterSnippetServiceHandlerClient registers the http handlers for service SnippetService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "SnippetServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "SnippetServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "SnippetServiceClient" to call the correct interceptors. +func RegisterSnippetServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client SnippetServiceClient) error { + + mux.Handle("POST", pattern_SnippetService_CreateCondition_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/snippetservice.pb.SnippetService/CreateCondition", runtime.WithHTTPPathPattern("/snippet-service/api/v1/conditions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SnippetService_CreateCondition_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_CreateCondition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_SnippetService_GetConditions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/snippetservice.pb.SnippetService/GetConditions", runtime.WithHTTPPathPattern("/snippet-service/api/v1/conditions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SnippetService_GetConditions_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_GetConditions_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("DELETE", pattern_SnippetService_DeleteConditions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/snippetservice.pb.SnippetService/DeleteConditions", runtime.WithHTTPPathPattern("/snippet-service/api/v1/conditions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SnippetService_DeleteConditions_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_DeleteConditions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PUT", pattern_SnippetService_UpdateCondition_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/snippetservice.pb.SnippetService/UpdateCondition", runtime.WithHTTPPathPattern("/snippet-service/api/v1/conditions/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SnippetService_UpdateCondition_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_UpdateCondition_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_SnippetService_CreateConfiguration_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/snippetservice.pb.SnippetService/CreateConfiguration", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SnippetService_CreateConfiguration_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_CreateConfiguration_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_SnippetService_GetConfigurations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/snippetservice.pb.SnippetService/GetConfigurations", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SnippetService_GetConfigurations_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_GetConfigurations_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("DELETE", pattern_SnippetService_DeleteConfigurations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/snippetservice.pb.SnippetService/DeleteConfigurations", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SnippetService_DeleteConfigurations_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_DeleteConfigurations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("PUT", pattern_SnippetService_UpdateConfiguration_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/snippetservice.pb.SnippetService/UpdateConfiguration", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations/{id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SnippetService_UpdateConfiguration_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_UpdateConfiguration_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_SnippetService_InvokeConfiguration_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/snippetservice.pb.SnippetService/InvokeConfiguration", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations/{configuration_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SnippetService_InvokeConfiguration_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_InvokeConfiguration_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_SnippetService_GetAppliedConfigurations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/snippetservice.pb.SnippetService/GetAppliedConfigurations", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations/applied")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SnippetService_GetAppliedConfigurations_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_GetAppliedConfigurations_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("DELETE", pattern_SnippetService_DeleteAppliedConfigurations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/snippetservice.pb.SnippetService/DeleteAppliedConfigurations", runtime.WithHTTPPathPattern("/snippet-service/api/v1/configurations/applied")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SnippetService_DeleteAppliedConfigurations_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_SnippetService_DeleteAppliedConfigurations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_SnippetService_CreateCondition_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"snippet-service", "api", "v1", "conditions"}, "")) + + pattern_SnippetService_GetConditions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"snippet-service", "api", "v1", "conditions"}, "")) + + pattern_SnippetService_DeleteConditions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"snippet-service", "api", "v1", "conditions"}, "")) + + pattern_SnippetService_UpdateCondition_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"snippet-service", "api", "v1", "conditions", "id"}, "")) + + pattern_SnippetService_CreateConfiguration_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"snippet-service", "api", "v1", "configurations"}, "")) + + pattern_SnippetService_GetConfigurations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"snippet-service", "api", "v1", "configurations"}, "")) + + pattern_SnippetService_DeleteConfigurations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"snippet-service", "api", "v1", "configurations"}, "")) + + pattern_SnippetService_UpdateConfiguration_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"snippet-service", "api", "v1", "configurations", "id"}, "")) + + pattern_SnippetService_InvokeConfiguration_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"snippet-service", "api", "v1", "configurations", "configuration_id"}, "")) + + pattern_SnippetService_GetAppliedConfigurations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"snippet-service", "api", "v1", "configurations", "applied"}, "")) + + pattern_SnippetService_DeleteAppliedConfigurations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"snippet-service", "api", "v1", "configurations", "applied"}, "")) +) + +var ( + forward_SnippetService_CreateCondition_0 = runtime.ForwardResponseMessage + + forward_SnippetService_GetConditions_0 = runtime.ForwardResponseStream + + forward_SnippetService_DeleteConditions_0 = runtime.ForwardResponseMessage + + forward_SnippetService_UpdateCondition_0 = runtime.ForwardResponseMessage + + forward_SnippetService_CreateConfiguration_0 = runtime.ForwardResponseMessage + + forward_SnippetService_GetConfigurations_0 = runtime.ForwardResponseStream + + forward_SnippetService_DeleteConfigurations_0 = runtime.ForwardResponseMessage + + forward_SnippetService_UpdateConfiguration_0 = runtime.ForwardResponseMessage + + forward_SnippetService_InvokeConfiguration_0 = runtime.ForwardResponseMessage + + forward_SnippetService_GetAppliedConfigurations_0 = runtime.ForwardResponseStream + + forward_SnippetService_DeleteAppliedConfigurations_0 = runtime.ForwardResponseMessage +) diff --git a/snippet-service/pb/service.proto b/snippet-service/pb/service.proto new file mode 100644 index 000000000..d1f1d1bae --- /dev/null +++ b/snippet-service/pb/service.proto @@ -0,0 +1,284 @@ +syntax = "proto3"; + +package snippetservice.pb; + +import "resource-aggregate/pb/resources.proto"; +import "resource-aggregate/pb/events.proto"; + +import "google/api/annotations.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "PLGD Rule Engine API"; + version: "1.0"; + description: "API for configuring rule engine in PLGD"; + contact: { + name: "plgd.dev"; + url: "https://github.com/plgd-dev/hub"; + email: "info@plgd.dev"; + }; + license: { + name: "Apache License 2.0"; + url: "https://github.com/plgd-dev/hub/blob/v2/LICENSE"; + }; + }; + schemes: [HTTPS]; + consumes: ["application/json", "application/protojson"]; + produces: ["application/json", "application/protojson"]; +}; + +option go_package = "github.com/plgd-dev/hub/v2/snippet-service/pb;pb"; + +message IDFilter { + string id = 1; + oneof version { + uint64 value = 2; + bool all = 3; + bool latest = 4; + } +} + +message Condition { // driven by resource change event + // Condition ID + string id = 1; + // Condition version + uint64 version = 2; + // User-friendly condition name + string name = 3; + // Condition is enabled/disabled + bool enabled = 4; + // ID of the configuration to be applied when the condition is satisfied + string configuration_id = 5; + // list of device IDs to which the condition applies + repeated string device_id_filter = 6; + repeated string resource_type_filter = 7; + // list of resource hrefs to which the condition applies + repeated string resource_href_filter = 8; + string jq_expression_filter = 9; + // Token used to update resources in the configuration + string api_access_token = 10; + // Condition owner + string owner = 11; + // Unix timestamp in ns when the condition has been created/updated + int64 timestamp = 12; +} + +message GetConditionsRequest { + repeated IDFilter id_filter = 1; + // returns latest conditions for given configurationId + repeated string configuration_id_filter = 2; + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + repeated string http_id_filter = 3 [ deprecated = true ]; +} + +message DeleteConditionsRequest { + repeated IDFilter id_filter = 1; + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + repeated string http_id_filter = 2 [ deprecated = true ]; +} + +message DeleteConditionsResponse { bool success = 1; } + +message Configuration { + message Resource { + // href of the resource + string href = 1; + // content update of the resource + resourceaggregate.pb.Content content = 2; + // optional update command time to live, 0 is infinite + int64 time_to_live = 3; + } + // Configuration ID + string id = 1; + // Configuration version + uint64 version = 2; + // User-friendly configuration name + string name = 3; + // List of resource updates to be applied + repeated Resource resources = 4; + // Configuration owner + string owner = 5; + // Unix timestamp in ns when the configuration has been created/updated + int64 timestamp = 6; +} + +message GetConfigurationsRequest { + repeated IDFilter id_filter = 1; + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + repeated string http_id_filter = 2 [ deprecated = true ]; +} +message DeleteConfigurationsRequest { + repeated IDFilter id_filter = 1; + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + repeated string http_id_filter = 2 [ deprecated = true ]; +} +message DeleteConfigurationsResponse { bool success = 1; } + +message AppliedConfiguration { + message Resource { + string href = 1; + // Reused from invoke command or generated. Can be used to retrieve corresponding pending command. + string correlation_id = 2; // @gotags: bson:"correlationId" + enum Status { + UNSPECIFIED = 0; + QUEUED = 1; + PENDING = 2; + DONE = 3; // If done look to resource_updated if update resource failed for resource aggregate. + TIMEOUT = 4; + }; + Status status = 3; + resourceaggregate.pb.ResourceUpdated resource_updated = 4; // @gotags: bson:"resourceUpdated,omitempty" + // Unix nanoseconds timestamp for resource in PENDING status, until which the pending update is valid + int64 valid_until = 5; // @gotags: bson:"validUntil,omitempty" + } + string id = 1; + string device_id = 2; // @gotags: bson:"deviceId" + message LinkedTo { + string id = 1; + uint64 version = 2; + } + LinkedTo configuration_id = 3; // @gotags: bson:"configurationId" + oneof executed_by { + bool on_demand = 4; + LinkedTo condition_id = 5; // @gotags: bson:"conditionId" + } + repeated Resource resources = 6; + string owner = 7; + // Unix timestamp in ns when the applied device configuration has been created/updated + int64 timestamp = 8; +} + +message InvokeConfigurationRequest { + string configuration_id = 1; // applies latest configuration + string device_id = 2; + bool force = 3; // force update even if the configuration has already been applied to device + string correlation_id = 4; // propagated down to the resource update command +} + +message InvokeConfigurationResponse { + string applied_configuration_id = 1; +} + +message GetAppliedConfigurationsRequest { + repeated string id_filter = 1; + repeated string device_id_filter = 2; + repeated IDFilter configuration_id_filter = 3; + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + repeated string http_configuration_id_filter = 4 [ deprecated = true ]; + repeated IDFilter condition_id_filter = 5; + // Format: {id}/{version}, e.g., "ae424c58-e517-4494-6de7-583536c48213/all" or "ae424c58-e517-4494-6de7-583536c48213/latest" or "ae424c58-e517-4494-6de7-583536c48213/1234" + repeated string http_condition_id_filter = 6 [ deprecated = true ]; +} + +message DeleteAppliedConfigurationsRequest { repeated string id_filter = 1; } +message DeleteAppliedConfigurationsResponse { bool success = 1; } + +service SnippetService { + rpc CreateCondition(Condition) returns (Condition) { + option (google.api.http) = { + post: "/snippet-service/api/v1/conditions"; + body: "*"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Conditions" ]; + }; + } + + rpc GetConditions(GetConditionsRequest) returns (stream Condition) { + option (google.api.http) = { + get: "/snippet-service/api/v1/conditions"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Conditions" ]; + }; + } + + rpc DeleteConditions(DeleteConditionsRequest) returns (DeleteConditionsResponse) { + option (google.api.http) = { + delete: "/snippet-service/api/v1/conditions"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Conditions" ]; + }; + } + + // For update the condition whole condition is required and the version must be incremented. + rpc UpdateCondition(Condition) returns (Condition) { + option (google.api.http) = { + put: "/snippet-service/api/v1/conditions/{id}"; + body: "*"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Conditions" ]; + }; + } + + rpc CreateConfiguration(Configuration) returns (Configuration) { + option (google.api.http) = { + post: "/snippet-service/api/v1/configurations"; + body: "*"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Configurations" ]; + }; + } + + rpc GetConfigurations(GetConfigurationsRequest) returns (stream Configuration) { + option (google.api.http) = { + get: "/snippet-service/api/v1/configurations"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Configurations" ]; + }; + } + + rpc DeleteConfigurations(DeleteConfigurationsRequest) returns (DeleteConfigurationsResponse) { + option (google.api.http) = { + delete: "/snippet-service/api/v1/configurations"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Configurations" ]; + }; + } + + // For update the configuration whole configuration is required and the version must be incremented. + rpc UpdateConfiguration(Configuration) returns (Configuration) { + option (google.api.http) = { + put: "/snippet-service/api/v1/configurations/{id}"; + body: "*"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Configurations" ]; + }; + } + + // streaming process of update configuration to invoker + rpc InvokeConfiguration(InvokeConfigurationRequest) returns (InvokeConfigurationResponse) { + option (google.api.http) = { + post: "/snippet-service/api/v1/configurations/{configuration_id}"; + body: "*"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Configurations" ]; + }; + } + + rpc GetAppliedConfigurations(GetAppliedConfigurationsRequest) returns (stream AppliedConfiguration) { + option (google.api.http) = { + get: "/snippet-service/api/v1/configurations/applied"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Configurations" ]; + }; + } + + rpc DeleteAppliedConfigurations(DeleteAppliedConfigurationsRequest) returns (DeleteAppliedConfigurationsResponse) { + option (google.api.http) = { + delete: "/snippet-service/api/v1/configurations/applied"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Configurations" ]; + }; + } +} diff --git a/snippet-service/pb/service.swagger.json b/snippet-service/pb/service.swagger.json new file mode 100644 index 000000000..a0c93e113 --- /dev/null +++ b/snippet-service/pb/service.swagger.json @@ -0,0 +1,997 @@ +{ + "swagger": "2.0", + "info": { + "title": "PLGD Rule Engine API", + "description": "API for configuring rule engine in PLGD", + "version": "1.0", + "contact": { + "name": "plgd.dev", + "url": "https://github.com/plgd-dev/hub", + "email": "info@plgd.dev" + }, + "license": { + "name": "Apache License 2.0", + "url": "https://github.com/plgd-dev/hub/blob/v2/LICENSE" + } + }, + "tags": [ + { + "name": "SnippetService" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json", + "application/protojson" + ], + "produces": [ + "application/json", + "application/protojson" + ], + "paths": { + "/snippet-service/api/v1/conditions": { + "get": { + "operationId": "SnippetService_GetConditions", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/pbCondition" + }, + "error": { + "$ref": "#/definitions/googlerpcStatus" + } + }, + "title": "Stream result of pbCondition" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "configurationIdFilter", + "description": "returns latest conditions for given configurationId", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "httpIdFilter", + "description": "Format: {id}/{version}, e.g., \"ae424c58-e517-4494-6de7-583536c48213/all\" or \"ae424c58-e517-4494-6de7-583536c48213/latest\" or \"ae424c58-e517-4494-6de7-583536c48213/1234\"", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Conditions" + ] + }, + "delete": { + "operationId": "SnippetService_DeleteConditions", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbDeleteConditionsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "httpIdFilter", + "description": "Format: {id}/{version}, e.g., \"ae424c58-e517-4494-6de7-583536c48213/all\" or \"ae424c58-e517-4494-6de7-583536c48213/latest\" or \"ae424c58-e517-4494-6de7-583536c48213/1234\"", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Conditions" + ] + }, + "post": { + "operationId": "SnippetService_CreateCondition", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbCondition" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pbCondition" + } + } + ], + "tags": [ + "Conditions" + ] + } + }, + "/snippet-service/api/v1/conditions/{id}": { + "put": { + "summary": "For update the condition whole condition is required and the version must be incremented.", + "operationId": "SnippetService_UpdateCondition", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbCondition" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "description": "Condition ID", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SnippetServiceUpdateConditionBody" + } + } + ], + "tags": [ + "Conditions" + ] + } + }, + "/snippet-service/api/v1/configurations": { + "get": { + "operationId": "SnippetService_GetConfigurations", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/pbConfiguration" + }, + "error": { + "$ref": "#/definitions/googlerpcStatus" + } + }, + "title": "Stream result of pbConfiguration" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "httpIdFilter", + "description": "Format: {id}/{version}, e.g., \"ae424c58-e517-4494-6de7-583536c48213/all\" or \"ae424c58-e517-4494-6de7-583536c48213/latest\" or \"ae424c58-e517-4494-6de7-583536c48213/1234\"", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Configurations" + ] + }, + "delete": { + "operationId": "SnippetService_DeleteConfigurations", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbDeleteConfigurationsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "httpIdFilter", + "description": "Format: {id}/{version}, e.g., \"ae424c58-e517-4494-6de7-583536c48213/all\" or \"ae424c58-e517-4494-6de7-583536c48213/latest\" or \"ae424c58-e517-4494-6de7-583536c48213/1234\"", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Configurations" + ] + }, + "post": { + "operationId": "SnippetService_CreateConfiguration", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbConfiguration" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pbConfiguration" + } + } + ], + "tags": [ + "Configurations" + ] + } + }, + "/snippet-service/api/v1/configurations/applied": { + "get": { + "operationId": "SnippetService_GetAppliedConfigurations", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/pbAppliedConfiguration" + }, + "error": { + "$ref": "#/definitions/googlerpcStatus" + } + }, + "title": "Stream result of pbAppliedConfiguration" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "idFilter", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "deviceIdFilter", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "httpConfigurationIdFilter", + "description": "Format: {id}/{version}, e.g., \"ae424c58-e517-4494-6de7-583536c48213/all\" or \"ae424c58-e517-4494-6de7-583536c48213/latest\" or \"ae424c58-e517-4494-6de7-583536c48213/1234\"", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "httpConditionIdFilter", + "description": "Format: {id}/{version}, e.g., \"ae424c58-e517-4494-6de7-583536c48213/all\" or \"ae424c58-e517-4494-6de7-583536c48213/latest\" or \"ae424c58-e517-4494-6de7-583536c48213/1234\"", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Configurations" + ] + }, + "delete": { + "operationId": "SnippetService_DeleteAppliedConfigurations", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbDeleteAppliedConfigurationsResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "idFilter", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "tags": [ + "Configurations" + ] + } + }, + "/snippet-service/api/v1/configurations/{configurationId}": { + "post": { + "summary": "streaming process of update configuration to invoker", + "operationId": "SnippetService_InvokeConfiguration", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbInvokeConfigurationResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "configurationId", + "description": "applies latest configuration", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SnippetServiceInvokeConfigurationBody" + } + } + ], + "tags": [ + "Configurations" + ] + } + }, + "/snippet-service/api/v1/configurations/{id}": { + "put": { + "summary": "For update the configuration whole configuration is required and the version must be incremented.", + "operationId": "SnippetService_UpdateConfiguration", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbConfiguration" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "description": "Configuration ID", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SnippetServiceUpdateConfigurationBody" + } + } + ], + "tags": [ + "Configurations" + ] + } + } + }, + "definitions": { + "AppliedConfigurationLinkedTo": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "string", + "format": "uint64" + } + } + }, + "AppliedConfigurationResourceStatus": { + "type": "string", + "enum": [ + "UNSPECIFIED", + "QUEUED", + "PENDING", + "DONE", + "TIMEOUT" + ], + "default": "UNSPECIFIED", + "description": " - DONE: If done look to resource_updated if update resource failed for resource aggregate." + }, + "SnippetServiceInvokeConfigurationBody": { + "type": "object", + "properties": { + "deviceId": { + "type": "string" + }, + "force": { + "type": "boolean", + "title": "force update even if the configuration has already been applied to device" + }, + "correlationId": { + "type": "string", + "title": "propagated down to the resource update command" + } + } + }, + "SnippetServiceUpdateConditionBody": { + "type": "object", + "properties": { + "version": { + "type": "string", + "format": "uint64", + "title": "Condition version" + }, + "name": { + "type": "string", + "title": "User-friendly condition name" + }, + "enabled": { + "type": "boolean", + "title": "Condition is enabled/disabled" + }, + "configurationId": { + "type": "string", + "title": "ID of the configuration to be applied when the condition is satisfied" + }, + "deviceIdFilter": { + "type": "array", + "items": { + "type": "string" + }, + "title": "list of device IDs to which the condition applies" + }, + "resourceTypeFilter": { + "type": "array", + "items": { + "type": "string" + } + }, + "resourceHrefFilter": { + "type": "array", + "items": { + "type": "string" + }, + "title": "list of resource hrefs to which the condition applies" + }, + "jqExpressionFilter": { + "type": "string" + }, + "apiAccessToken": { + "type": "string", + "title": "Token used to update resources in the configuration" + }, + "owner": { + "type": "string", + "title": "Condition owner" + }, + "timestamp": { + "type": "string", + "format": "int64", + "title": "Unix timestamp in ns when the condition has been created/updated" + } + }, + "title": "driven by resource change event" + }, + "SnippetServiceUpdateConfigurationBody": { + "type": "object", + "properties": { + "version": { + "type": "string", + "format": "uint64", + "title": "Configuration version" + }, + "name": { + "type": "string", + "title": "User-friendly configuration name" + }, + "resources": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/pbConfigurationResource" + }, + "title": "List of resource updates to be applied" + }, + "owner": { + "type": "string", + "title": "Configuration owner" + }, + "timestamp": { + "type": "string", + "format": "int64", + "title": "Unix timestamp in ns when the configuration has been created/updated" + } + } + }, + "googlerpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "pbAppliedConfiguration": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "deviceId": { + "type": "string", + "title": "@gotags: bson:\"deviceId\"" + }, + "configurationId": { + "$ref": "#/definitions/AppliedConfigurationLinkedTo", + "title": "@gotags: bson:\"configurationId\"" + }, + "onDemand": { + "type": "boolean" + }, + "conditionId": { + "$ref": "#/definitions/AppliedConfigurationLinkedTo", + "title": "@gotags: bson:\"conditionId\"" + }, + "resources": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/pbAppliedConfigurationResource" + } + }, + "owner": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "int64", + "title": "Unix timestamp in ns when the applied device configuration has been created/updated" + } + } + }, + "pbAppliedConfigurationResource": { + "type": "object", + "properties": { + "href": { + "type": "string" + }, + "correlationId": { + "type": "string", + "description": "Reused from invoke command or generated. Can be used to retrieve corresponding pending command.\n\n@gotags: bson:\"correlationId\"" + }, + "status": { + "$ref": "#/definitions/AppliedConfigurationResourceStatus" + }, + "resourceUpdated": { + "$ref": "#/definitions/pbResourceUpdated", + "title": "@gotags: bson:\"resourceUpdated,omitempty\"" + }, + "validUntil": { + "type": "string", + "format": "int64", + "description": "@gotags: bson:\"validUntil,omitempty\"", + "title": "Unix nanoseconds timestamp for resource in PENDING status, until which the pending update is valid" + } + } + }, + "pbAuditContext": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "correlationId": { + "type": "string" + }, + "owner": { + "type": "string" + } + } + }, + "pbCondition": { + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "Condition ID" + }, + "version": { + "type": "string", + "format": "uint64", + "title": "Condition version" + }, + "name": { + "type": "string", + "title": "User-friendly condition name" + }, + "enabled": { + "type": "boolean", + "title": "Condition is enabled/disabled" + }, + "configurationId": { + "type": "string", + "title": "ID of the configuration to be applied when the condition is satisfied" + }, + "deviceIdFilter": { + "type": "array", + "items": { + "type": "string" + }, + "title": "list of device IDs to which the condition applies" + }, + "resourceTypeFilter": { + "type": "array", + "items": { + "type": "string" + } + }, + "resourceHrefFilter": { + "type": "array", + "items": { + "type": "string" + }, + "title": "list of resource hrefs to which the condition applies" + }, + "jqExpressionFilter": { + "type": "string" + }, + "apiAccessToken": { + "type": "string", + "title": "Token used to update resources in the configuration" + }, + "owner": { + "type": "string", + "title": "Condition owner" + }, + "timestamp": { + "type": "string", + "format": "int64", + "title": "Unix timestamp in ns when the condition has been created/updated" + } + }, + "title": "driven by resource change event" + }, + "pbConfiguration": { + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "Configuration ID" + }, + "version": { + "type": "string", + "format": "uint64", + "title": "Configuration version" + }, + "name": { + "type": "string", + "title": "User-friendly configuration name" + }, + "resources": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/pbConfigurationResource" + }, + "title": "List of resource updates to be applied" + }, + "owner": { + "type": "string", + "title": "Configuration owner" + }, + "timestamp": { + "type": "string", + "format": "int64", + "title": "Unix timestamp in ns when the configuration has been created/updated" + } + } + }, + "pbConfigurationResource": { + "type": "object", + "properties": { + "href": { + "type": "string", + "title": "href of the resource" + }, + "content": { + "$ref": "#/definitions/pbContent", + "title": "content update of the resource" + }, + "timeToLive": { + "type": "string", + "format": "int64", + "title": "optional update command time to live, 0 is infinite" + } + } + }, + "pbContent": { + "type": "object", + "properties": { + "data": { + "type": "string", + "format": "byte" + }, + "contentType": { + "type": "string" + }, + "coapContentFormat": { + "type": "integer", + "format": "int32", + "title": "-1 means content-format was not provided" + } + } + }, + "pbDeleteAppliedConfigurationsResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + }, + "pbDeleteConditionsResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + }, + "pbDeleteConfigurationsResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + }, + "pbEventMetadata": { + "type": "object", + "properties": { + "version": { + "type": "string", + "format": "uint64" + }, + "timestamp": { + "type": "string", + "format": "int64" + }, + "connectionId": { + "type": "string" + }, + "sequence": { + "type": "string", + "format": "uint64", + "title": "sequence number within the same connection_id; the ResourceChanged event uses the value to skip old events, other event types might not fill the value" + }, + "hubId": { + "type": "string", + "title": "the hub which sent the event" + } + } + }, + "pbIDFilter": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "value": { + "type": "string", + "format": "uint64" + }, + "all": { + "type": "boolean" + }, + "latest": { + "type": "boolean" + } + } + }, + "pbInvokeConfigurationResponse": { + "type": "object", + "properties": { + "appliedConfigurationId": { + "type": "string" + } + } + }, + "pbResourceId": { + "type": "object", + "properties": { + "deviceId": { + "type": "string" + }, + "href": { + "type": "string" + } + } + }, + "pbResourceUpdated": { + "type": "object", + "properties": { + "resourceId": { + "$ref": "#/definitions/pbResourceId" + }, + "status": { + "$ref": "#/definitions/resourceaggregatepbStatus" + }, + "content": { + "$ref": "#/definitions/pbContent" + }, + "auditContext": { + "$ref": "#/definitions/pbAuditContext" + }, + "eventMetadata": { + "$ref": "#/definitions/pbEventMetadata" + }, + "resourceTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "openTelemetryCarrier": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "title": "Open telemetry data propagated to asynchronous events" + } + } + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "resourceaggregatepbStatus": { + "type": "string", + "enum": [ + "UNKNOWN", + "OK", + "BAD_REQUEST", + "UNAUTHORIZED", + "FORBIDDEN", + "NOT_FOUND", + "UNAVAILABLE", + "NOT_IMPLEMENTED", + "ACCEPTED", + "ERROR", + "METHOD_NOT_ALLOWED", + "CREATED", + "CANCELED", + "NOT_MODIFIED" + ], + "default": "UNKNOWN", + "description": " - CANCELED: Canceled indicates the operation was canceled (typically by the user).\n - NOT_MODIFIED: Valid indicates the content hasn't changed. (provided etag in GET request is same as the resource etag)." + } + } +} diff --git a/snippet-service/pb/service_grpc.pb.go b/snippet-service/pb/service_grpc.pb.go new file mode 100644 index 000000000..6b06ada8c --- /dev/null +++ b/snippet-service/pb/service_grpc.pb.go @@ -0,0 +1,567 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v5.26.1 +// source: snippet-service/pb/service.proto + +package pb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + SnippetService_CreateCondition_FullMethodName = "/snippetservice.pb.SnippetService/CreateCondition" + SnippetService_GetConditions_FullMethodName = "/snippetservice.pb.SnippetService/GetConditions" + SnippetService_DeleteConditions_FullMethodName = "/snippetservice.pb.SnippetService/DeleteConditions" + SnippetService_UpdateCondition_FullMethodName = "/snippetservice.pb.SnippetService/UpdateCondition" + SnippetService_CreateConfiguration_FullMethodName = "/snippetservice.pb.SnippetService/CreateConfiguration" + SnippetService_GetConfigurations_FullMethodName = "/snippetservice.pb.SnippetService/GetConfigurations" + SnippetService_DeleteConfigurations_FullMethodName = "/snippetservice.pb.SnippetService/DeleteConfigurations" + SnippetService_UpdateConfiguration_FullMethodName = "/snippetservice.pb.SnippetService/UpdateConfiguration" + SnippetService_InvokeConfiguration_FullMethodName = "/snippetservice.pb.SnippetService/InvokeConfiguration" + SnippetService_GetAppliedConfigurations_FullMethodName = "/snippetservice.pb.SnippetService/GetAppliedConfigurations" + SnippetService_DeleteAppliedConfigurations_FullMethodName = "/snippetservice.pb.SnippetService/DeleteAppliedConfigurations" +) + +// SnippetServiceClient is the client API for SnippetService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type SnippetServiceClient interface { + CreateCondition(ctx context.Context, in *Condition, opts ...grpc.CallOption) (*Condition, error) + GetConditions(ctx context.Context, in *GetConditionsRequest, opts ...grpc.CallOption) (SnippetService_GetConditionsClient, error) + DeleteConditions(ctx context.Context, in *DeleteConditionsRequest, opts ...grpc.CallOption) (*DeleteConditionsResponse, error) + // For update the condition whole condition is required and the version must be incremented. + UpdateCondition(ctx context.Context, in *Condition, opts ...grpc.CallOption) (*Condition, error) + CreateConfiguration(ctx context.Context, in *Configuration, opts ...grpc.CallOption) (*Configuration, error) + GetConfigurations(ctx context.Context, in *GetConfigurationsRequest, opts ...grpc.CallOption) (SnippetService_GetConfigurationsClient, error) + DeleteConfigurations(ctx context.Context, in *DeleteConfigurationsRequest, opts ...grpc.CallOption) (*DeleteConfigurationsResponse, error) + // For update the configuration whole configuration is required and the version must be incremented. + UpdateConfiguration(ctx context.Context, in *Configuration, opts ...grpc.CallOption) (*Configuration, error) + // streaming process of update configuration to invoker + InvokeConfiguration(ctx context.Context, in *InvokeConfigurationRequest, opts ...grpc.CallOption) (*InvokeConfigurationResponse, error) + GetAppliedConfigurations(ctx context.Context, in *GetAppliedConfigurationsRequest, opts ...grpc.CallOption) (SnippetService_GetAppliedConfigurationsClient, error) + DeleteAppliedConfigurations(ctx context.Context, in *DeleteAppliedConfigurationsRequest, opts ...grpc.CallOption) (*DeleteAppliedConfigurationsResponse, error) +} + +type snippetServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewSnippetServiceClient(cc grpc.ClientConnInterface) SnippetServiceClient { + return &snippetServiceClient{cc} +} + +func (c *snippetServiceClient) CreateCondition(ctx context.Context, in *Condition, opts ...grpc.CallOption) (*Condition, error) { + out := new(Condition) + err := c.cc.Invoke(ctx, SnippetService_CreateCondition_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *snippetServiceClient) GetConditions(ctx context.Context, in *GetConditionsRequest, opts ...grpc.CallOption) (SnippetService_GetConditionsClient, error) { + stream, err := c.cc.NewStream(ctx, &SnippetService_ServiceDesc.Streams[0], SnippetService_GetConditions_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &snippetServiceGetConditionsClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type SnippetService_GetConditionsClient interface { + Recv() (*Condition, error) + grpc.ClientStream +} + +type snippetServiceGetConditionsClient struct { + grpc.ClientStream +} + +func (x *snippetServiceGetConditionsClient) Recv() (*Condition, error) { + m := new(Condition) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *snippetServiceClient) DeleteConditions(ctx context.Context, in *DeleteConditionsRequest, opts ...grpc.CallOption) (*DeleteConditionsResponse, error) { + out := new(DeleteConditionsResponse) + err := c.cc.Invoke(ctx, SnippetService_DeleteConditions_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *snippetServiceClient) UpdateCondition(ctx context.Context, in *Condition, opts ...grpc.CallOption) (*Condition, error) { + out := new(Condition) + err := c.cc.Invoke(ctx, SnippetService_UpdateCondition_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *snippetServiceClient) CreateConfiguration(ctx context.Context, in *Configuration, opts ...grpc.CallOption) (*Configuration, error) { + out := new(Configuration) + err := c.cc.Invoke(ctx, SnippetService_CreateConfiguration_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *snippetServiceClient) GetConfigurations(ctx context.Context, in *GetConfigurationsRequest, opts ...grpc.CallOption) (SnippetService_GetConfigurationsClient, error) { + stream, err := c.cc.NewStream(ctx, &SnippetService_ServiceDesc.Streams[1], SnippetService_GetConfigurations_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &snippetServiceGetConfigurationsClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type SnippetService_GetConfigurationsClient interface { + Recv() (*Configuration, error) + grpc.ClientStream +} + +type snippetServiceGetConfigurationsClient struct { + grpc.ClientStream +} + +func (x *snippetServiceGetConfigurationsClient) Recv() (*Configuration, error) { + m := new(Configuration) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *snippetServiceClient) DeleteConfigurations(ctx context.Context, in *DeleteConfigurationsRequest, opts ...grpc.CallOption) (*DeleteConfigurationsResponse, error) { + out := new(DeleteConfigurationsResponse) + err := c.cc.Invoke(ctx, SnippetService_DeleteConfigurations_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *snippetServiceClient) UpdateConfiguration(ctx context.Context, in *Configuration, opts ...grpc.CallOption) (*Configuration, error) { + out := new(Configuration) + err := c.cc.Invoke(ctx, SnippetService_UpdateConfiguration_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *snippetServiceClient) InvokeConfiguration(ctx context.Context, in *InvokeConfigurationRequest, opts ...grpc.CallOption) (*InvokeConfigurationResponse, error) { + out := new(InvokeConfigurationResponse) + err := c.cc.Invoke(ctx, SnippetService_InvokeConfiguration_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *snippetServiceClient) GetAppliedConfigurations(ctx context.Context, in *GetAppliedConfigurationsRequest, opts ...grpc.CallOption) (SnippetService_GetAppliedConfigurationsClient, error) { + stream, err := c.cc.NewStream(ctx, &SnippetService_ServiceDesc.Streams[2], SnippetService_GetAppliedConfigurations_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &snippetServiceGetAppliedConfigurationsClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type SnippetService_GetAppliedConfigurationsClient interface { + Recv() (*AppliedConfiguration, error) + grpc.ClientStream +} + +type snippetServiceGetAppliedConfigurationsClient struct { + grpc.ClientStream +} + +func (x *snippetServiceGetAppliedConfigurationsClient) Recv() (*AppliedConfiguration, error) { + m := new(AppliedConfiguration) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *snippetServiceClient) DeleteAppliedConfigurations(ctx context.Context, in *DeleteAppliedConfigurationsRequest, opts ...grpc.CallOption) (*DeleteAppliedConfigurationsResponse, error) { + out := new(DeleteAppliedConfigurationsResponse) + err := c.cc.Invoke(ctx, SnippetService_DeleteAppliedConfigurations_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SnippetServiceServer is the server API for SnippetService service. +// All implementations must embed UnimplementedSnippetServiceServer +// for forward compatibility +type SnippetServiceServer interface { + CreateCondition(context.Context, *Condition) (*Condition, error) + GetConditions(*GetConditionsRequest, SnippetService_GetConditionsServer) error + DeleteConditions(context.Context, *DeleteConditionsRequest) (*DeleteConditionsResponse, error) + // For update the condition whole condition is required and the version must be incremented. + UpdateCondition(context.Context, *Condition) (*Condition, error) + CreateConfiguration(context.Context, *Configuration) (*Configuration, error) + GetConfigurations(*GetConfigurationsRequest, SnippetService_GetConfigurationsServer) error + DeleteConfigurations(context.Context, *DeleteConfigurationsRequest) (*DeleteConfigurationsResponse, error) + // For update the configuration whole configuration is required and the version must be incremented. + UpdateConfiguration(context.Context, *Configuration) (*Configuration, error) + // streaming process of update configuration to invoker + InvokeConfiguration(context.Context, *InvokeConfigurationRequest) (*InvokeConfigurationResponse, error) + GetAppliedConfigurations(*GetAppliedConfigurationsRequest, SnippetService_GetAppliedConfigurationsServer) error + DeleteAppliedConfigurations(context.Context, *DeleteAppliedConfigurationsRequest) (*DeleteAppliedConfigurationsResponse, error) + mustEmbedUnimplementedSnippetServiceServer() +} + +// UnimplementedSnippetServiceServer must be embedded to have forward compatible implementations. +type UnimplementedSnippetServiceServer struct { +} + +func (UnimplementedSnippetServiceServer) CreateCondition(context.Context, *Condition) (*Condition, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateCondition not implemented") +} +func (UnimplementedSnippetServiceServer) GetConditions(*GetConditionsRequest, SnippetService_GetConditionsServer) error { + return status.Errorf(codes.Unimplemented, "method GetConditions not implemented") +} +func (UnimplementedSnippetServiceServer) DeleteConditions(context.Context, *DeleteConditionsRequest) (*DeleteConditionsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteConditions not implemented") +} +func (UnimplementedSnippetServiceServer) UpdateCondition(context.Context, *Condition) (*Condition, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateCondition not implemented") +} +func (UnimplementedSnippetServiceServer) CreateConfiguration(context.Context, *Configuration) (*Configuration, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateConfiguration not implemented") +} +func (UnimplementedSnippetServiceServer) GetConfigurations(*GetConfigurationsRequest, SnippetService_GetConfigurationsServer) error { + return status.Errorf(codes.Unimplemented, "method GetConfigurations not implemented") +} +func (UnimplementedSnippetServiceServer) DeleteConfigurations(context.Context, *DeleteConfigurationsRequest) (*DeleteConfigurationsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteConfigurations not implemented") +} +func (UnimplementedSnippetServiceServer) UpdateConfiguration(context.Context, *Configuration) (*Configuration, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateConfiguration not implemented") +} +func (UnimplementedSnippetServiceServer) InvokeConfiguration(context.Context, *InvokeConfigurationRequest) (*InvokeConfigurationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method InvokeConfiguration not implemented") +} +func (UnimplementedSnippetServiceServer) GetAppliedConfigurations(*GetAppliedConfigurationsRequest, SnippetService_GetAppliedConfigurationsServer) error { + return status.Errorf(codes.Unimplemented, "method GetAppliedConfigurations not implemented") +} +func (UnimplementedSnippetServiceServer) DeleteAppliedConfigurations(context.Context, *DeleteAppliedConfigurationsRequest) (*DeleteAppliedConfigurationsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteAppliedConfigurations not implemented") +} +func (UnimplementedSnippetServiceServer) mustEmbedUnimplementedSnippetServiceServer() {} + +// UnsafeSnippetServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SnippetServiceServer will +// result in compilation errors. +type UnsafeSnippetServiceServer interface { + mustEmbedUnimplementedSnippetServiceServer() +} + +func RegisterSnippetServiceServer(s grpc.ServiceRegistrar, srv SnippetServiceServer) { + s.RegisterService(&SnippetService_ServiceDesc, srv) +} + +func _SnippetService_CreateCondition_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Condition) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SnippetServiceServer).CreateCondition(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SnippetService_CreateCondition_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SnippetServiceServer).CreateCondition(ctx, req.(*Condition)) + } + return interceptor(ctx, in, info, handler) +} + +func _SnippetService_GetConditions_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetConditionsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(SnippetServiceServer).GetConditions(m, &snippetServiceGetConditionsServer{stream}) +} + +type SnippetService_GetConditionsServer interface { + Send(*Condition) error + grpc.ServerStream +} + +type snippetServiceGetConditionsServer struct { + grpc.ServerStream +} + +func (x *snippetServiceGetConditionsServer) Send(m *Condition) error { + return x.ServerStream.SendMsg(m) +} + +func _SnippetService_DeleteConditions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteConditionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SnippetServiceServer).DeleteConditions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SnippetService_DeleteConditions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SnippetServiceServer).DeleteConditions(ctx, req.(*DeleteConditionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SnippetService_UpdateCondition_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Condition) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SnippetServiceServer).UpdateCondition(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SnippetService_UpdateCondition_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SnippetServiceServer).UpdateCondition(ctx, req.(*Condition)) + } + return interceptor(ctx, in, info, handler) +} + +func _SnippetService_CreateConfiguration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Configuration) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SnippetServiceServer).CreateConfiguration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SnippetService_CreateConfiguration_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SnippetServiceServer).CreateConfiguration(ctx, req.(*Configuration)) + } + return interceptor(ctx, in, info, handler) +} + +func _SnippetService_GetConfigurations_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetConfigurationsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(SnippetServiceServer).GetConfigurations(m, &snippetServiceGetConfigurationsServer{stream}) +} + +type SnippetService_GetConfigurationsServer interface { + Send(*Configuration) error + grpc.ServerStream +} + +type snippetServiceGetConfigurationsServer struct { + grpc.ServerStream +} + +func (x *snippetServiceGetConfigurationsServer) Send(m *Configuration) error { + return x.ServerStream.SendMsg(m) +} + +func _SnippetService_DeleteConfigurations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteConfigurationsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SnippetServiceServer).DeleteConfigurations(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SnippetService_DeleteConfigurations_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SnippetServiceServer).DeleteConfigurations(ctx, req.(*DeleteConfigurationsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SnippetService_UpdateConfiguration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Configuration) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SnippetServiceServer).UpdateConfiguration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SnippetService_UpdateConfiguration_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SnippetServiceServer).UpdateConfiguration(ctx, req.(*Configuration)) + } + return interceptor(ctx, in, info, handler) +} + +func _SnippetService_InvokeConfiguration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(InvokeConfigurationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SnippetServiceServer).InvokeConfiguration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SnippetService_InvokeConfiguration_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SnippetServiceServer).InvokeConfiguration(ctx, req.(*InvokeConfigurationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SnippetService_GetAppliedConfigurations_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetAppliedConfigurationsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(SnippetServiceServer).GetAppliedConfigurations(m, &snippetServiceGetAppliedConfigurationsServer{stream}) +} + +type SnippetService_GetAppliedConfigurationsServer interface { + Send(*AppliedConfiguration) error + grpc.ServerStream +} + +type snippetServiceGetAppliedConfigurationsServer struct { + grpc.ServerStream +} + +func (x *snippetServiceGetAppliedConfigurationsServer) Send(m *AppliedConfiguration) error { + return x.ServerStream.SendMsg(m) +} + +func _SnippetService_DeleteAppliedConfigurations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteAppliedConfigurationsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SnippetServiceServer).DeleteAppliedConfigurations(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SnippetService_DeleteAppliedConfigurations_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SnippetServiceServer).DeleteAppliedConfigurations(ctx, req.(*DeleteAppliedConfigurationsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SnippetService_ServiceDesc is the grpc.ServiceDesc for SnippetService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SnippetService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "snippetservice.pb.SnippetService", + HandlerType: (*SnippetServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateCondition", + Handler: _SnippetService_CreateCondition_Handler, + }, + { + MethodName: "DeleteConditions", + Handler: _SnippetService_DeleteConditions_Handler, + }, + { + MethodName: "UpdateCondition", + Handler: _SnippetService_UpdateCondition_Handler, + }, + { + MethodName: "CreateConfiguration", + Handler: _SnippetService_CreateConfiguration_Handler, + }, + { + MethodName: "DeleteConfigurations", + Handler: _SnippetService_DeleteConfigurations_Handler, + }, + { + MethodName: "UpdateConfiguration", + Handler: _SnippetService_UpdateConfiguration_Handler, + }, + { + MethodName: "InvokeConfiguration", + Handler: _SnippetService_InvokeConfiguration_Handler, + }, + { + MethodName: "DeleteAppliedConfigurations", + Handler: _SnippetService_DeleteAppliedConfigurations_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "GetConditions", + Handler: _SnippetService_GetConditions_Handler, + ServerStreams: true, + }, + { + StreamName: "GetConfigurations", + Handler: _SnippetService_GetConfigurations_Handler, + ServerStreams: true, + }, + { + StreamName: "GetAppliedConfigurations", + Handler: _SnippetService_GetAppliedConfigurations_Handler, + ServerStreams: true, + }, + }, + Metadata: "snippet-service/pb/service.proto", +} diff --git a/snippet-service/pb/tags.go b/snippet-service/pb/tags.go new file mode 100644 index 000000000..6558ad1ac --- /dev/null +++ b/snippet-service/pb/tags.go @@ -0,0 +1,28 @@ +package pb + +const ( + RecordIDKey = "_id" + IDKey = "id" // must match with Id field tag + DeviceIDKey = "deviceId" // must match with DeviceId field tag + OwnerKey = "owner" // must match with Owner field tag + LatestKey = "latest" // must match with Latest field tag + VersionKey = "version" // must match with Version field tag + VersionsKey = "versions" // must match with Versions field tag + ResourcesKey = "resources" // must match with Resources field tag + ConfigurationIDKey = "configurationId" // must match with ConfigurationId field tag + ConditionIDKey = "conditionId" // must match with ConditionId field tag + EnabledKey = "enabled" // must match with Enabled field tag + TimestampKey = "timestamp" // must match with Timestamp field tag + StatusKey = "status" // must match with Status field tag + HrefKey = "href" // must match with Href field tag + ValidUntil = "validUntil" // must match with ValidUntil field tag + + DeviceIDFilterKey = "deviceIdFilter" // must match with Condition.DeviceIdFilter tag + ResourceHrefFilterKey = "resourceHrefFilter" // must match with Condition.ResourceHrefFilter tag + ResourceTypeFilterKey = "resourceTypeFilter" // must match with Condition.ResourceTypeFilter tag + + ConfigurationLinkIDKey = ConfigurationIDKey + ".id" // configurationId.id + ConfigurationLinkVersionKey = ConfigurationIDKey + "." + VersionKey // configurationId.version + ConditionLinkIDKey = ConditionIDKey + ".id" // conditionId.id + ConditionLinkVersionKey = ConditionIDKey + "." + VersionKey // conditionId.version +) diff --git a/snippet-service/service/config.go b/snippet-service/service/config.go new file mode 100644 index 000000000..a3f5e42ed --- /dev/null +++ b/snippet-service/service/config.go @@ -0,0 +1,111 @@ +package service + +import ( + "fmt" + "net" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/pkg/config" + "github.com/plgd-dev/hub/v2/pkg/log" + httpServer "github.com/plgd-dev/hub/v2/pkg/net/http/server" + otelClient "github.com/plgd-dev/hub/v2/pkg/opentelemetry/collector/client" + natsClient "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus/nats/client" + grpcService "github.com/plgd-dev/hub/v2/snippet-service/service/grpc" + storeConfig "github.com/plgd-dev/hub/v2/snippet-service/store/config" + "github.com/plgd-dev/hub/v2/snippet-service/updater" +) + +type HTTPConfig struct { + Addr string `yaml:"address" json:"address"` + Server httpServer.Config `yaml:",inline" json:",inline"` +} + +func (c *HTTPConfig) Validate() error { + if _, err := net.ResolveTCPAddr("tcp", c.Addr); err != nil { + return fmt.Errorf("address('%v') - %w", c.Addr, err) + } + return nil +} + +// Config represent application configuration +type APIsConfig struct { + GRPC grpcService.Config `yaml:"grpc" json:"grpc"` + HTTP HTTPConfig `yaml:"http" json:"http"` +} + +func (c *APIsConfig) Validate() error { + if err := c.GRPC.Validate(); err != nil { + return fmt.Errorf("grpc.%w", err) + } + if err := c.HTTP.Validate(); err != nil { + return fmt.Errorf("http.%w", err) + } + return nil +} + +type EventBusConfig struct { + NATS natsClient.Config `yaml:"nats" json:"nats"` + SubscriptionID string `yaml:"subscriptionID" json:"subscriptionID"` +} + +func (c *EventBusConfig) Validate() error { + if err := c.NATS.Validate(); err != nil { + return fmt.Errorf("nats.%w", err) + } + if c.SubscriptionID == "" { + return fmt.Errorf("subscriptionID('%v')", c.SubscriptionID) + } + return nil +} + +type ClientsConfig struct { + Storage storeConfig.Config `yaml:"storage" json:"storage"` + OpenTelemetryCollector otelClient.Config `yaml:"openTelemetryCollector" json:"openTelemetryCollector"` + EventBus EventBusConfig `yaml:"eventBus" json:"eventBus"` + ResourceUpdater updater.ResourceUpdaterConfig `yaml:"resourceUpdater" json:"resourceUpdater"` +} + +func (c *ClientsConfig) Validate() error { + if err := c.Storage.Validate(); err != nil { + return fmt.Errorf("storage.%w", err) + } + if err := c.OpenTelemetryCollector.Validate(); err != nil { + return fmt.Errorf("openTelemetryCollector.%w", err) + } + if err := c.EventBus.Validate(); err != nil { + return fmt.Errorf("eventBus.%w", err) + } + if err := c.ResourceUpdater.Validate(); err != nil { + return fmt.Errorf("resourceUpdater.%w", err) + } + return nil +} + +type Config struct { + HubID string `yaml:"hubID" json:"hubId"` + Log log.Config `yaml:"log" json:"log"` + APIs APIsConfig `yaml:"apis" json:"apis"` + Clients ClientsConfig `yaml:"clients" json:"clients"` +} + +func (c *Config) Validate() error { + if err := c.Log.Validate(); err != nil { + return fmt.Errorf("log.%w", err) + } + if err := c.APIs.Validate(); err != nil { + return fmt.Errorf("apis.%w", err) + } + if err := c.Clients.Validate(); err != nil { + return fmt.Errorf("clients.%w", err) + } + if _, err := uuid.Parse(c.HubID); err != nil { + return fmt.Errorf("hubID('%v') - %w", c.HubID, err) + } + + return nil +} + +// String return string representation of Config +func (c Config) String() string { + return config.ToString(c) +} diff --git a/snippet-service/service/config_test.go b/snippet-service/service/config_test.go new file mode 100644 index 000000000..d324b5740 --- /dev/null +++ b/snippet-service/service/config_test.go @@ -0,0 +1,257 @@ +package service_test + +import ( + "testing" + + "github.com/plgd-dev/hub/v2/pkg/log" + grpcServer "github.com/plgd-dev/hub/v2/pkg/net/grpc/server" + otelClient "github.com/plgd-dev/hub/v2/pkg/opentelemetry/collector/client" + natsClient "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus/nats/client" + "github.com/plgd-dev/hub/v2/snippet-service/service" + storeConfig "github.com/plgd-dev/hub/v2/snippet-service/store/config" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/snippet-service/updater" + "github.com/stretchr/testify/require" +) + +func TestAPIsConfig(t *testing.T) { + tests := []struct { + name string + cfg service.APIsConfig + wantErr bool + }{ + { + name: "valid", + cfg: test.MakeAPIsConfig(), + wantErr: false, + }, + { + name: "invalid - bad http", + cfg: func() service.APIsConfig { + cfg := test.MakeAPIsConfig() + cfg.HTTP = service.HTTPConfig{ + Addr: "bad", + } + return cfg + }(), + wantErr: true, + }, + { + name: "invalid - no grpc", + cfg: func() service.APIsConfig { + cfg := test.MakeAPIsConfig() + cfg.GRPC = grpcServer.Config{} + return cfg + }(), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestHTTPConfig(t *testing.T) { + tests := []struct { + name string + cfg service.HTTPConfig + wantErr bool + }{ + { + name: "valid", + cfg: test.MakeHTTPConfig(), + wantErr: false, + }, + { + name: "invalid - bad address", + cfg: func() service.HTTPConfig { + cfg := test.MakeHTTPConfig() + cfg.Addr = "bad" + return cfg + }(), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestStorageConfig(t *testing.T) { + tests := []struct { + name string + cfg storeConfig.Config + wantErr bool + }{ + { + name: "valid", + cfg: test.MakeStoreConfig(), + wantErr: false, + }, + { + name: "invalid", + cfg: storeConfig.Config{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestClientsConfig(t *testing.T) { + tests := []struct { + name string + cfg service.ClientsConfig + wantErr bool + }{ + { + name: "valid", + cfg: test.MakeClientsConfig(), + }, + { + name: "invalid - no storage", + cfg: func() service.ClientsConfig { + cfg := test.MakeClientsConfig() + cfg.Storage = storeConfig.Config{} + return cfg + }(), + wantErr: true, + }, + { + name: "invalid open telemetry", + cfg: func() service.ClientsConfig { + cfg := test.MakeClientsConfig() + cfg.OpenTelemetryCollector = otelClient.Config{ + GRPC: otelClient.GRPCConfig{ + Enabled: true, + }, + } + return cfg + }(), + wantErr: true, + }, + { + name: "invalid NATS", + cfg: func() service.ClientsConfig { + cfg := test.MakeClientsConfig() + cfg.EventBus.NATS = natsClient.Config{ + URL: "bad", + } + return cfg + }(), + wantErr: true, + }, + { + name: "invalid SubscriptionID", + cfg: func() service.ClientsConfig { + cfg := test.MakeClientsConfig() + cfg.EventBus.SubscriptionID = "" + return cfg + }(), + wantErr: true, + }, + { + name: "invalid ResourceUpdater", + cfg: func() service.ClientsConfig { + cfg := test.MakeClientsConfig() + cfg.ResourceUpdater = updater.ResourceUpdaterConfig{} + return cfg + }(), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestConfig(t *testing.T) { + tests := []struct { + name string + cfg service.Config + wantErr bool + }{ + { + name: "valid", + cfg: test.MakeConfig(t), + wantErr: false, + }, + { + name: "invalid - bad log", + cfg: func() service.Config { + cfg := test.MakeConfig(t) + cfg.Log = log.Config{ + Level: 42, + } + return cfg + }(), + wantErr: true, + }, + { + name: "invalid - no apis", + cfg: func() service.Config { + cfg := test.MakeConfig(t) + cfg.APIs = service.APIsConfig{} + return cfg + }(), + wantErr: true, + }, + { + name: "invalid - no clients", + cfg: func() service.Config { + cfg := test.MakeConfig(t) + cfg.Clients = service.ClientsConfig{} + return cfg + }(), + wantErr: true, + }, + { + name: "invalid - bad hubID", + cfg: func() service.Config { + cfg := test.MakeConfig(t) + cfg.HubID = "bad" + return cfg + }(), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/snippet-service/service/grpc/config.go b/snippet-service/service/grpc/config.go new file mode 100644 index 000000000..655e8170e --- /dev/null +++ b/snippet-service/service/grpc/config.go @@ -0,0 +1,7 @@ +package grpc + +import ( + "github.com/plgd-dev/hub/v2/pkg/net/grpc/server" +) + +type Config = server.Config diff --git a/snippet-service/service/grpc/server.go b/snippet-service/service/grpc/server.go new file mode 100644 index 000000000..dd39c3cc5 --- /dev/null +++ b/snippet-service/service/grpc/server.go @@ -0,0 +1,349 @@ +package grpc + +import ( + "context" + "errors" + "fmt" + + "github.com/plgd-dev/hub/v2/pkg/log" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/plgd-dev/hub/v2/snippet-service/updater" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// SnippetServiceServer handles incoming requests. +type SnippetServiceServer struct { + pb.UnimplementedSnippetServiceServer + + store store.Store + resourceUpdater *updater.ResourceUpdater + ownerClaim string + hubID string + logger log.Logger +} + +func NewSnippetServiceServer(store store.Store, resourceUpdater *updater.ResourceUpdater, ownerClaim string, hubID string, logger log.Logger) *SnippetServiceServer { + return &SnippetServiceServer{ + store: store, + resourceUpdater: resourceUpdater, + logger: logger, + ownerClaim: ownerClaim, + hubID: hubID, + } +} + +func (s *SnippetServiceServer) checkOwner(ctx context.Context, owner string) (string, error) { + ownerFromToken, err := pkgGrpc.OwnerFromTokenMD(ctx, s.ownerClaim) + if err != nil { + return "", err + } + if owner != "" && ownerFromToken != owner { + return "", fmt.Errorf("owner mismatch: expected %v, got %v", owner, ownerFromToken) + } + return ownerFromToken, nil +} + +func getGRPCErrorCode(err error) codes.Code { + if errors.Is(err, store.ErrInvalidArgument) { + return codes.InvalidArgument + } + return codes.Internal +} + +func errCannotCreateConfiguration(err error) error { + return fmt.Errorf("cannot get configuration: %w", err) +} + +func (s *SnippetServiceServer) CreateConfiguration(ctx context.Context, conf *pb.Configuration) (*pb.Configuration, error) { + owner, err := s.checkOwner(ctx, conf.GetOwner()) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "%v", errCannotCreateConfiguration(err))) + } + + conf.Owner = owner + c, err := s.store.CreateConfiguration(ctx, conf) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(getGRPCErrorCode(err), "%v", errCannotCreateConfiguration(err))) + } + return c, nil +} + +func errCannotUpdateConfiguration(err error) error { + return fmt.Errorf("cannot update configuration: %w", err) +} + +func (s *SnippetServiceServer) UpdateConfiguration(ctx context.Context, conf *pb.Configuration) (*pb.Configuration, error) { + owner, err := s.checkOwner(ctx, conf.GetOwner()) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "%v", errCannotUpdateConfiguration(err))) + } + + conf.Owner = owner + // increment version automatically by mongo + conf.Version = 0 + c, err := s.store.UpdateConfiguration(ctx, conf) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(getGRPCErrorCode(err), "%v", errCannotUpdateConfiguration(err))) + } + return c, nil +} + +func errCannotGetConfigurations(err error) error { + return fmt.Errorf("cannot get configurations: %w", err) +} + +func sendConfiguration(srv pb.SnippetService_GetConfigurationsServer, sc *store.Configuration) error { + var err error + var lastVersion *pb.Configuration + sc.RangeVersions(func(_ int, c *pb.Configuration) bool { + errS := srv.Send(c) + if errS != nil { + err = errS + return false + } + lastVersion = c + return true + }) + if err != nil { + return err + } + if sc.Latest == nil { + return nil + } + latest, err := sc.GetLatest() + if err != nil { + return err + } + if lastVersion != nil && lastVersion.GetVersion() == latest.GetVersion() { + // already sent when iterating over versions array + return nil + } + return srv.Send(latest) +} + +func getAllLatest() []*pb.IDFilter { + return []*pb.IDFilter{ + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + } +} + +func (s *SnippetServiceServer) GetConfigurations(req *pb.GetConfigurationsRequest, srv pb.SnippetService_GetConfigurationsServer) error { + owner, err := s.checkOwner(srv.Context(), "") + if err != nil { + return s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "%v", errCannotGetConfigurations(err))) + } + req.IdFilter = append(req.GetIdFilter(), req.ConvertHTTPIDFilter()...) + if len(req.GetIdFilter()) == 0 { + // get all latest conditions by default + req.IdFilter = getAllLatest() + } + + err = s.store.GetConfigurations(srv.Context(), owner, req, func(c *store.Configuration) error { + return sendConfiguration(srv, c) + }) + if err != nil { + return s.logger.LogAndReturnError(status.Errorf(codes.Internal, "%v", errCannotGetConfigurations(err))) + } + return nil +} + +func errCannotDeleteConfigurations(err error) error { + return fmt.Errorf("cannot delete configurations: %w", err) +} + +func (s *SnippetServiceServer) DeleteConfigurations(ctx context.Context, req *pb.DeleteConfigurationsRequest) (*pb.DeleteConfigurationsResponse, error) { + owner, err := s.checkOwner(ctx, "") + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "%v", errCannotDeleteConfigurations(err))) + } + req.IdFilter = append(req.GetIdFilter(), req.ConvertHTTPIDFilter()...) + + err = s.store.DeleteConfigurations(ctx, owner, req) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.Internal, "%v", errCannotDeleteConfigurations(err))) + } + return &pb.DeleteConfigurationsResponse{ + Success: true, + }, nil +} + +func errCannotInvokeConfiguration(err error) error { + return fmt.Errorf("cannot invoke configuration: %w", err) +} + +func (s *SnippetServiceServer) InvokeConfiguration(ctx context.Context, req *pb.InvokeConfigurationRequest) (*pb.InvokeConfigurationResponse, error) { + owner, err := s.checkOwner(ctx, "") + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "%v", errCannotInvokeConfiguration(err))) + } + token, errT := pkgGrpc.TokenFromMD(ctx) + // we must have token for communication by raClient + if errT != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.Internal, "%v", errCannotInvokeConfiguration(errT))) + } + appliedConf, err := s.resourceUpdater.InvokeConfiguration(ctx, token, owner, req) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.Internal, "%v", errCannotInvokeConfiguration(err))) + } + return &pb.InvokeConfigurationResponse{ + AppliedConfigurationId: appliedConf.GetId(), + }, nil +} + +func errCannotCreateCondition(err error) error { + return fmt.Errorf("cannot create condition: %w", err) +} + +func (s *SnippetServiceServer) CreateCondition(ctx context.Context, condition *pb.Condition) (*pb.Condition, error) { + owner, err := s.checkOwner(ctx, condition.GetOwner()) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "%v", errCannotCreateCondition(err))) + } + + condition.Owner = owner + c, err := s.store.CreateCondition(ctx, condition) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(getGRPCErrorCode(err), "%v", errCannotCreateCondition(err))) + } + return c, nil +} + +func errCannotUpdateCondition(err error) error { + return fmt.Errorf("cannot update condition: %w", err) +} + +func (s *SnippetServiceServer) UpdateCondition(ctx context.Context, condition *pb.Condition) (*pb.Condition, error) { + owner, err := s.checkOwner(ctx, condition.GetOwner()) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "%v", errCannotUpdateCondition(err))) + } + + condition.Owner = owner + // increment version automatically by mongo + condition.Version = 0 + c, err := s.store.UpdateCondition(ctx, condition) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(getGRPCErrorCode(err), "%v", errCannotUpdateCondition(err))) + } + return c, nil +} + +func errCannotGetConditions(err error) error { + return fmt.Errorf("cannot get conditions: %w", err) +} + +func sendCondition(srv pb.SnippetService_GetConditionsServer, sc *store.Condition) error { + var err error + var lastVersion *pb.Condition + sc.RangeVersions(func(_ int, c *pb.Condition) bool { + errS := srv.Send(c) + if errS != nil { + err = errS + return false + } + lastVersion = c + return true + }) + if err != nil { + return err + } + if sc.Latest == nil { + return nil + } + latest, err := sc.GetLatest() + if err != nil { + return err + } + if lastVersion != nil && lastVersion.GetVersion() == latest.GetVersion() { + // already sent when iterating over versions array + return nil + } + return srv.Send(latest) +} + +func (s *SnippetServiceServer) GetConditions(req *pb.GetConditionsRequest, srv pb.SnippetService_GetConditionsServer) error { + owner, err := s.checkOwner(srv.Context(), "") + if err != nil { + return s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "%v", errCannotGetConditions(err))) + } + req.IdFilter = append(req.GetIdFilter(), req.ConvertHTTPIDFilter()...) + if len(req.GetIdFilter()) == 0 && len(req.GetConfigurationIdFilter()) == 0 { + // get all latest conditions by default + req.IdFilter = getAllLatest() + } + + err = s.store.GetConditions(srv.Context(), owner, req, func(c *store.Condition) error { + return sendCondition(srv, c) + }) + if err != nil { + return s.logger.LogAndReturnError(status.Errorf(codes.Internal, "%v", errCannotGetConditions(err))) + } + return nil +} + +func errCannotDeleteConditions(err error) error { + return fmt.Errorf("cannot delete conditions: %w", err) +} + +func (s *SnippetServiceServer) DeleteConditions(ctx context.Context, req *pb.DeleteConditionsRequest) (*pb.DeleteConditionsResponse, error) { + owner, err := s.checkOwner(ctx, "") + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "%v", errCannotDeleteConditions(err))) + } + req.IdFilter = append(req.GetIdFilter(), req.ConvertHTTPIDFilter()...) + + err = s.store.DeleteConditions(ctx, owner, req) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.Internal, "%v", errCannotDeleteConditions(err))) + } + return &pb.DeleteConditionsResponse{ + Success: true, + }, nil +} + +func errCannotGetAppliedConfigurations(err error) error { + return fmt.Errorf("cannot get applied configurations: %w", err) +} + +func (s *SnippetServiceServer) GetAppliedConfigurations(req *pb.GetAppliedConfigurationsRequest, srv pb.SnippetService_GetAppliedConfigurationsServer) error { + owner, err := s.checkOwner(srv.Context(), "") + if err != nil { + return s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "%v", errCannotGetAppliedConfigurations(err))) + } + + req.ConditionIdFilter = append(req.GetConditionIdFilter(), req.ConvertHTTPConditionIdFilter()...) + req.ConfigurationIdFilter = append(req.GetConfigurationIdFilter(), req.ConvertHTTPConfigurationIdFilter()...) + + err = s.store.GetAppliedConfigurations(srv.Context(), owner, req, func(c *store.AppliedConfiguration) error { + return srv.Send(c.GetAppliedConfiguration()) + }) + if err != nil { + return s.logger.LogAndReturnError(status.Errorf(codes.Internal, "%v", errCannotGetAppliedConfigurations(err))) + } + return nil +} + +func errCannotDeleteAppliedConfigurations(err error) error { + return fmt.Errorf("cannot delete applied configurations: %w", err) +} + +func (s *SnippetServiceServer) DeleteAppliedConfigurations(ctx context.Context, req *pb.DeleteAppliedConfigurationsRequest) (*pb.DeleteAppliedConfigurationsResponse, error) { + owner, err := s.checkOwner(ctx, "") + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "%v", errCannotDeleteAppliedConfigurations(err))) + } + err = s.store.DeleteAppliedConfigurations(ctx, owner, req) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.Internal, "%v", errCannotDeleteAppliedConfigurations(err))) + } + return &pb.DeleteAppliedConfigurationsResponse{ + Success: true, + }, nil +} diff --git a/snippet-service/service/grpc/service.go b/snippet-service/service/grpc/service.go new file mode 100644 index 000000000..0ee12344e --- /dev/null +++ b/snippet-service/service/grpc/service.go @@ -0,0 +1,35 @@ +package grpc + +import ( + "fmt" + + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/net/grpc/server" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "go.opentelemetry.io/otel/trace" +) + +type Service struct { + *server.Server +} + +func New(config Config, snippetServiceServer *SnippetServiceServer, validator *validator.Validator, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Service, error) { + opts, err := server.MakeDefaultOptions(server.NewAuth(validator), logger, tracerProvider) + if err != nil { + return nil, fmt.Errorf("cannot create grpc server options: %w", err) + } + server, err := server.New(config, fileWatcher, logger, opts...) + if err != nil { + return nil, err + } + pb.RegisterSnippetServiceServer(server.Server, snippetServiceServer) + + // SnippetService needs to stop gracefully to ensure that all commands are processed. + server.SetGracefulStop(true) + + return &Service{ + Server: server, + }, nil +} diff --git a/snippet-service/service/http/config.go b/snippet-service/service/http/config.go new file mode 100644 index 000000000..5ae74d064 --- /dev/null +++ b/snippet-service/service/http/config.go @@ -0,0 +1,13 @@ +package http + +import ( + "github.com/plgd-dev/hub/v2/pkg/net/http/server" + "github.com/plgd-dev/hub/v2/pkg/net/listener" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" +) + +type Config struct { + Connection listener.Config `yaml:",inline" json:",inline"` + Authorization validator.Config `yaml:"authorization" json:"authorization"` + Server server.Config `yaml:",inline" json:",inline"` +} diff --git a/snippet-service/service/http/createCondition_test.go b/snippet-service/service/http/createCondition_test.go new file mode 100644 index 000000000..4d85fb4ba --- /dev/null +++ b/snippet-service/service/http/createCondition_test.go @@ -0,0 +1,154 @@ +package http_test + +import ( + "bytes" + "context" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + snippetPb "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + snippetTest "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" +) + +func makeCreateCondition(id, configurationID, owner string) *snippetPb.Condition { + return &snippetPb.Condition{ + Id: id, + Name: snippetTest.ConditionName(0), + Enabled: true, + ConfigurationId: configurationID, + Owner: owner, + DeviceIdFilter: snippetTest.ConditionDeviceIdFilter(2, 2), + ResourceTypeFilter: snippetTest.ConditionResourceTypeFilter(2, 2), + ResourceHrefFilter: snippetTest.ConditionResourceHrefFilter(2, 2), + JqExpressionFilter: snippetTest.ConditionJqExpressionFilter(0), + ApiAccessToken: snippetTest.ConditionApiAccessToken(0), + } +} + +func TestRequestHandlerCreateCondition(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + defer shutDown() + + snippetCfg := snippetTest.MakeConfig(t) + _, shutdownHttp := snippetTest.New(t, snippetCfg) + defer shutdownHttp() + + token := oauthTest.GetDefaultAccessToken(t) + condID1 := uuid.NewString() + confID1 := uuid.NewString() + condID2 := uuid.NewString() + confID2 := uuid.NewString() + + type args struct { + cond *snippetPb.Condition + token string + } + tests := []struct { + name string + args args + wantHTTPCode int + want *snippetPb.Condition + wantErr bool + }{ + { + name: "create", + args: args{ + cond: makeCreateCondition(condID1, confID1, ""), + token: token, + }, + wantHTTPCode: http.StatusOK, + want: makeCreateCondition(condID1, confID1, oauthService.DeviceUserID), + }, + { + name: "create - with owner", + args: args{ + cond: makeCreateCondition(condID2, confID2, oauthService.DeviceUserID), + token: token, + }, + wantHTTPCode: http.StatusOK, + want: makeCreateCondition(condID2, confID2, oauthService.DeviceUserID), + }, + { + name: "create - generate ID", + args: args{ + cond: makeCreateCondition("", uuid.NewString(), ""), + token: token, + }, + wantHTTPCode: http.StatusOK, + want: makeCreateCondition("", "", oauthService.DeviceUserID), + }, + { + name: "non-matching owner", + args: args{ + cond: makeCreateCondition(uuid.NewString(), uuid.NewString(), "non-matching-owner"), + token: token, + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + { + name: "duplicit ID", + args: args{ + cond: makeCreateCondition(condID1, uuid.NewString(), ""), + token: token, + }, + wantHTTPCode: http.StatusInternalServerError, + wantErr: true, + }, + { + name: "missing configuration id", + args: args{ + cond: makeCreateCondition(uuid.NewString(), "", ""), + token: token, + }, + wantHTTPCode: http.StatusBadRequest, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := httpTest.GetContentData(&pb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, tt.args.cond), + }, message.AppJSON.String()) + require.NoError(t, err) + rb := httpTest.NewRequest(http.MethodPost, snippetTest.HTTPURI(snippetHttp.Conditions), bytes.NewReader(data)).AuthToken(token) + rb = rb.ContentType(message.AppOcfCbor.String()) + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + var got snippetPb.Condition + err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + if tt.want.GetId() == "" { + tt.want.Id = got.GetId() + } + if tt.want.GetConfigurationId() == "" { + tt.want.ConfigurationId = got.GetConfigurationId() + } + snippetTest.CmpCondition(t, tt.want, &got, true) + }) + } +} diff --git a/snippet-service/service/http/createConfiguration_test.go b/snippet-service/service/http/createConfiguration_test.go new file mode 100644 index 000000000..c54472cb2 --- /dev/null +++ b/snippet-service/service/http/createConfiguration_test.go @@ -0,0 +1,205 @@ +package http_test + +import ( + "bytes" + "context" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + snippetPb "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + snippetTest "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" +) + +func makeTestResource(t *testing.T, href string, power int) *snippetPb.Configuration_Resource { + return &snippetPb.Configuration_Resource{ + Href: href, + Content: &commands.Content{ + Data: test.EncodeToCbor(t, map[string]interface{}{"power": power}), + ContentType: message.AppOcfCbor.String(), + CoapContentFormat: int32(message.AppOcfCbor), + }, + TimeToLive: 60, + } +} + +func TestRequestHandlerCreateConfiguration(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + defer shutDown() + + snippetCfg := snippetTest.MakeConfig(t) + _, shutdownHttp := snippetTest.New(t, snippetCfg) + defer shutdownHttp() + + token := oauthTest.GetDefaultAccessToken(t) + confID1 := uuid.NewString() + + conf1 := &snippetPb.Configuration{ + Id: confID1, + Name: "1st", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/1", 41), + }, + } + conf2 := &snippetPb.Configuration{ + Id: uuid.NewString(), + Owner: oauthService.DeviceUserID, + Name: "2nd", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/2", 42), + }, + } + conf3 := &snippetPb.Configuration{ + Name: "3rd", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/3", 43), + }, + } + + type args struct { + conf interface{} + token string + } + tests := []struct { + name string + args args + wantHTTPCode int + wantErr bool + want *snippetPb.Configuration + }{ + { + name: "create", + args: args{ + conf: conf1, + token: token, + }, + wantHTTPCode: http.StatusOK, + want: conf1, + }, + { + name: "create - with owner", + args: args{ + conf: conf2, + token: token, + }, + wantHTTPCode: http.StatusOK, + want: conf2, + }, + { + name: "create - generate ID", + args: args{ + conf: conf3, + token: token, + }, + wantHTTPCode: http.StatusOK, + want: conf3, + }, + { + name: "non-matching owner", + args: args{ + conf: &snippetPb.Configuration{ + Id: uuid.NewString(), + Owner: "non-matching-owner", + Name: "4th", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/4", 44), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + { + name: "duplicit ID", + args: args{ + conf: &snippetPb.Configuration{ + Id: confID1, + Name: "5th", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/5", 45), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusInternalServerError, + wantErr: true, + }, + { + name: "missing resources", + args: args{ + conf: &snippetPb.Configuration{ + Id: uuid.NewString(), + Name: "6th", + }, + token: token, + }, + wantHTTPCode: http.StatusBadRequest, + wantErr: true, + }, + { + name: "missing owner in token", + args: args{ + conf: &snippetPb.Configuration{ + Id: uuid.NewString(), + Name: "7th", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/7", 47), + }, + }, + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: nil, + }), + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := httpTest.GetContentData(&pb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, tt.args.conf), + }, message.AppJSON.String()) + require.NoError(t, err) + + rb := httpTest.NewRequest(http.MethodPost, snippetTest.HTTPURI(snippetHttp.Configurations), bytes.NewReader(data)).AuthToken(tt.args.token) + rb.Accept(pkgHttp.ApplicationProtoJsonContentType).ContentType(message.AppJSON.String()) + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + var got snippetPb.Configuration + err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + tt.want.Owner = oauthService.DeviceUserID + if tt.want.GetId() == "" { + tt.want.Id = got.GetId() + } + snippetTest.CmpConfiguration(t, tt.want, &got, true) + }) + } +} diff --git a/snippet-service/service/http/deleteAppliedConfigurations_test.go b/snippet-service/service/http/deleteAppliedConfigurations_test.go new file mode 100644 index 000000000..745c12420 --- /dev/null +++ b/snippet-service/service/http/deleteAppliedConfigurations_test.go @@ -0,0 +1,153 @@ +package http_test + +import ( + "context" + "crypto/tls" + "errors" + "io" + "net/http" + "testing" + + "github.com/plgd-dev/go-coap/v3/message" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + "github.com/plgd-dev/hub/v2/snippet-service/test" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestRequestHandlerDeleteAppliedConfigurations(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + defer shutDown() + + snippetCfg := test.MakeConfig(t) + ss, shutdownHttp := test.New(t, snippetCfg) + defer shutdownHttp() + + _ = test.AddAppliedConfigurationsToStore(ctx, t, ss.SnippetServiceStore()) + + conn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + ssc := pb.NewSnippetServiceClient(conn) + + getConfigurations := func(ctxWithToken context.Context) map[string]*pb.AppliedConfiguration { + getClient, errG := ssc.GetAppliedConfigurations(ctxWithToken, &pb.GetAppliedConfigurationsRequest{}) + require.NoError(t, errG) + defer func() { + _ = getClient.CloseSend() + }() + confs := make(map[string]*pb.AppliedConfiguration) + for { + conf, errR := getClient.Recv() + if errors.Is(errR, io.EOF) { + break + } + require.NoError(t, errR) + confs[conf.GetId()] = conf + } + return confs + } + + type args struct { + token string + idFilter []string + } + tests := []struct { + name string + args args + wantHTTPCode int + wantErr bool + want func() + }{ + { + name: "missing owner", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: nil, + }), + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + { + name: "owner1/all", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + }, + wantHTTPCode: http.StatusOK, + want: func() { + token := test.GetTokenWithOwnerClaim(t, test.Owner(1), snippetCfg.APIs.GRPC.Authorization.OwnerClaim) + ctxWithToken := pkgGrpc.CtxWithToken(ctx, token) + confs := getConfigurations(ctxWithToken) + require.Empty(t, confs) + }, + }, + { + name: "owner2/id{0,1,2,3,4,5}", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(2), + }), + idFilter: []string{ + test.AppliedConfigurationID(0), test.AppliedConfigurationID(1), test.AppliedConfigurationID(2), test.AppliedConfigurationID(3), + test.AppliedConfigurationID(4), test.AppliedConfigurationID(5), + }, + }, + wantHTTPCode: http.StatusOK, + want: func() { + token := test.GetTokenWithOwnerClaim(t, test.Owner(2), snippetCfg.APIs.GRPC.Authorization.OwnerClaim) + ctxWithToken := pkgGrpc.CtxWithToken(ctx, token) + confs := getConfigurations(ctxWithToken) + require.NotEmpty(t, confs) + for _, c := range confs { + owner := c.GetOwner() + require.Equal(t, test.Owner(2), owner) + id := c.GetId() + if id == test.AppliedConfigurationID(0) || id == test.AppliedConfigurationID(1) || id == test.AppliedConfigurationID(2) || + id == test.AppliedConfigurationID(3) || id == test.AppliedConfigurationID(4) || id == test.AppliedConfigurationID(5) { + require.FailNow(t, "configuration not deleted") + } + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rb := httpTest.NewRequest(http.MethodDelete, test.HTTPURI(snippetHttp.AppliedConfigurations), nil).AuthToken(tt.args.token) + rb = rb.Accept(pkgHttp.ApplicationProtoJsonContentType).ContentType(message.AppCBOR.String()).IDFilter(tt.args.idFilter) + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + var deleteResp pb.DeleteAppliedConfigurationsResponse + err := httpTest.Unmarshal(resp.StatusCode, resp.Body, &deleteResp) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tt.want() + }) + } +} diff --git a/snippet-service/service/http/deleteConditions_test.go b/snippet-service/service/http/deleteConditions_test.go new file mode 100644 index 000000000..40863d348 --- /dev/null +++ b/snippet-service/service/http/deleteConditions_test.go @@ -0,0 +1,147 @@ +package http_test + +import ( + "context" + "crypto/tls" + "errors" + "io" + "net/http" + "testing" + + "github.com/plgd-dev/go-coap/v3/message" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/snippet-service/uri" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestRequestHandlerDeleteConditions(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + defer shutDown() + + snippetCfg := test.MakeConfig(t) + _, shutdownHttp := test.New(t, snippetCfg) + defer shutdownHttp() + + conn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewSnippetServiceClient(conn) + _ = test.AddConditions(ctx, t, snippetCfg.APIs.GRPC.Authorization.OwnerClaim, c, 30, nil) + + type args struct { + token string + httpIDFilter []string + } + tests := []struct { + name string + args args + wantHTTPCode int + wantErr bool + want func(*testing.T) + }{ + { + name: "missing owner", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: nil, + }), + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + { + name: "delete certain condition", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + httpIDFilter: []string{ + test.ConditionID(1) + "/0", + }, + }, + wantHTTPCode: http.StatusOK, + want: func(t *testing.T) { + getClient, errG := c.GetConditions(ctx, &pb.GetConditionsRequest{}) + require.NoError(t, errG) + defer func() { + _ = getClient.CloseSend() + }() + var anyExists bool + for { + cond, errR := getClient.Recv() + if errors.Is(errR, io.EOF) { + break + } + if cond.GetId() == test.ConditionID(1) { + require.FailNow(t, "unexpected condition", "condition: %v", cond) + } + anyExists = true + } + require.True(t, anyExists) + }, + }, + { + name: "owner1/all", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + }, + wantHTTPCode: http.StatusOK, + want: func(t *testing.T) { + getClient, errG := c.GetConditions(ctx, &pb.GetConditionsRequest{}) + require.NoError(t, errG) + defer func() { + _ = getClient.CloseSend() + }() + for { + cond, errR := getClient.Recv() + if errors.Is(errR, io.EOF) { + break + } + require.FailNow(t, "unexpected condition", "condition: %v", cond) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rb := httpTest.NewRequest(http.MethodDelete, test.HTTPURI(snippetHttp.Conditions), nil).AuthToken(tt.args.token) + rb = rb.Accept(pkgHttp.ApplicationProtoJsonContentType).ContentType(message.AppCBOR.String()) + if len(tt.args.httpIDFilter) > 0 { + rb = rb.AddQuery(uri.HTTPIDFilterQueryKey, tt.args.httpIDFilter...) + } + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + var deleteResp pb.DeleteConditionsResponse + err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &deleteResp) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/snippet-service/service/http/deleteConfigurations_test.go b/snippet-service/service/http/deleteConfigurations_test.go new file mode 100644 index 000000000..0417087f9 --- /dev/null +++ b/snippet-service/service/http/deleteConfigurations_test.go @@ -0,0 +1,147 @@ +package http_test + +import ( + "context" + "crypto/tls" + "errors" + "io" + "net/http" + "testing" + + "github.com/plgd-dev/go-coap/v3/message" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/snippet-service/uri" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestRequestHandlerDeleteConfigurations(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + defer shutDown() + + snippetCfg := test.MakeConfig(t) + _, shutdownHttp := test.New(t, snippetCfg) + defer shutdownHttp() + + conn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewSnippetServiceClient(conn) + _ = test.AddConfigurations(ctx, t, snippetCfg.APIs.GRPC.Authorization.OwnerClaim, c, 30, nil) + + type args struct { + token string + httpIDFilter []string + } + tests := []struct { + name string + args args + wantHTTPCode int + wantErr bool + want func(*testing.T) + }{ + { + name: "missing owner", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: nil, + }), + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + { + name: "configuration/{1, version=0}", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + httpIDFilter: []string{ + test.ConfigurationID(1) + "/0", + }, + }, + wantHTTPCode: http.StatusOK, + want: func(t *testing.T) { + getClient, errG := c.GetConfigurations(ctx, &pb.GetConfigurationsRequest{}) + require.NoError(t, errG) + defer func() { + _ = getClient.CloseSend() + }() + var anyExists bool + for { + conf, errR := getClient.Recv() + if errors.Is(errR, io.EOF) { + break + } + if conf.GetId() == test.ConfigurationID(1) { + require.FailNow(t, "unexpected configuration", "configuration: %v", conf) + } + anyExists = true + } + require.True(t, anyExists) + }, + }, + { + name: "owner1/all", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + }, + wantHTTPCode: http.StatusOK, + want: func(t *testing.T) { + getClient, errG := c.GetConfigurations(ctx, &pb.GetConfigurationsRequest{}) + require.NoError(t, errG) + defer func() { + _ = getClient.CloseSend() + }() + for { + conf, errR := getClient.Recv() + if errors.Is(errR, io.EOF) { + break + } + require.FailNow(t, "unexpected configuration", "configuration: %v", conf) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rb := httpTest.NewRequest(http.MethodDelete, test.HTTPURI(snippetHttp.Configurations), nil).AuthToken(tt.args.token) + rb = rb.Accept(pkgHttp.ApplicationProtoJsonContentType).ContentType(message.AppCBOR.String()) + if len(tt.args.httpIDFilter) > 0 { + rb = rb.AddQuery(uri.HTTPIDFilterQueryKey, tt.args.httpIDFilter...) + } + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + var deleteResp pb.DeleteConfigurationsResponse + err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &deleteResp) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/snippet-service/service/http/getAppliedConfigurations_test.go b/snippet-service/service/http/getAppliedConfigurations_test.go new file mode 100644 index 000000000..de140f162 --- /dev/null +++ b/snippet-service/service/http/getAppliedConfigurations_test.go @@ -0,0 +1,180 @@ +package http_test + +import ( + "context" + "errors" + "io" + "net/http" + "testing" + + "github.com/plgd-dev/go-coap/v3/message" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/snippet-service/uri" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" +) + +func TestRequestHandlerGetAppliedConfigurations(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + defer shutDown() + + snippetCfg := test.MakeConfig(t) + ss, shutdownHttp := test.New(t, snippetCfg) + defer shutdownHttp() + + appliedConfs := test.AddAppliedConfigurationsToStore(ctx, t, ss.SnippetServiceStore()) + + type args struct { + token string + idFilter []string + httpConfigurationIdFilter []string + httpConditionIdFilter []string + } + tests := []struct { + name string + args args + wantHTTPCode int + wantErr bool + want func(*pb.AppliedConfiguration) bool + }{ + { + name: "missing owner", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: nil, + }), + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + { + name: "owner1", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + }, + wantHTTPCode: http.StatusOK, + want: func(ac *pb.AppliedConfiguration) bool { + return ac.GetOwner() == test.Owner(1) + }, + }, + { + name: "owner0/id{0,1,2,3,4,5}", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(0), + }), + idFilter: []string{ + test.AppliedConfigurationID(0), test.AppliedConfigurationID(1), test.AppliedConfigurationID(2), test.AppliedConfigurationID(3), + test.AppliedConfigurationID(4), test.AppliedConfigurationID(5), + }, + }, + wantHTTPCode: http.StatusOK, + want: func(ac *pb.AppliedConfiguration) bool { + acID := ac.GetId() + return ac.GetOwner() == test.Owner(0) && + (acID == test.AppliedConfigurationID(0) || acID == test.AppliedConfigurationID(1) || + acID == test.AppliedConfigurationID(2) || acID == test.AppliedConfigurationID(3) || + acID == test.AppliedConfigurationID(4) || acID == test.AppliedConfigurationID(5)) + }, + }, + { + name: "owner2/configurationId[{id:2,version:2},id:5]", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(2), + }), + httpConfigurationIdFilter: []string{ + test.ConfigurationID(2) + "/2", + test.ConfigurationID(5) + "/all", + }, + }, + wantHTTPCode: http.StatusOK, + want: func(ac *pb.AppliedConfiguration) bool { + acConfID := ac.GetConfigurationId().GetId() + acConfVersion := ac.GetConfigurationId().GetVersion() + return ac.GetOwner() == test.Owner(2) && + ((acConfID == test.ConfigurationID(2) && acConfVersion == 2) || + (acConfID == test.ConfigurationID(5))) + }, + }, + { + name: "owner0/conditionId[{id:3,version:3},{id:6},{version:13}]", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(0), + }), + httpConditionIdFilter: []string{ + test.ConditionID(3) + "/3", + test.ConditionID(6) + "/all", + "13", + }, + }, + wantHTTPCode: http.StatusOK, + want: func(ac *pb.AppliedConfiguration) bool { + acCondID := ac.GetConditionId().GetId() + acCondVersion := ac.GetConditionId().GetVersion() + return ac.GetOwner() == test.Owner(0) && + ((acCondID == test.ConditionID(3) && acCondVersion == 3) || + (acCondID == test.ConditionID(6)) || + (acCondVersion == 13)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rb := httpTest.NewRequest(http.MethodGet, test.HTTPURI(snippetHttp.AppliedConfigurations), nil).AuthToken(tt.args.token) + rb = rb.Accept(pkgHttp.ApplicationProtoJsonContentType).ContentType(message.AppCBOR.String()).IDFilter(tt.args.idFilter) + if len(tt.args.httpConfigurationIdFilter) > 0 { + rb = rb.AddQuery(uri.HTTPConfigurationIDFilter, tt.args.httpConfigurationIdFilter...) + } + if len(tt.args.httpConditionIdFilter) > 0 { + rb = rb.AddQuery(uri.HTTPConditionIDFilter, tt.args.httpConditionIdFilter...) + } + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + receivedConfs := make(map[string]*pb.AppliedConfiguration) + for { + var value pb.AppliedConfiguration + err := httpTest.Unmarshal(resp.StatusCode, resp.Body, &value) + if errors.Is(err, io.EOF) { + break + } + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + receivedConfs[value.GetId()] = &value + } + + stored := make(map[string]*pb.AppliedConfiguration) + for _, ac := range appliedConfs { + if tt.want(ac.GetAppliedConfiguration()) { + stored[ac.GetId()] = ac.GetAppliedConfiguration().Clone() + } + } + require.Len(t, receivedConfs, len(stored)) + for _, c := range receivedConfs { + ac, ok := stored[c.GetId()] + require.True(t, ok) + test.CmpAppliedDeviceConfiguration(t, ac, c, false) + } + }) + } +} diff --git a/snippet-service/service/http/getConditions_test.go b/snippet-service/service/http/getConditions_test.go new file mode 100644 index 000000000..ad6b07ea3 --- /dev/null +++ b/snippet-service/service/http/getConditions_test.go @@ -0,0 +1,158 @@ +package http_test + +import ( + "context" + "crypto/tls" + "errors" + "io" + "net/http" + "testing" + + "github.com/plgd-dev/go-coap/v3/message" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/snippet-service/uri" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestRequestHandlerGetConditions(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + defer shutDown() + + snippetCfg := test.MakeConfig(t) + _, shutdownHttp := test.New(t, snippetCfg) + defer shutdownHttp() + + conn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewSnippetServiceClient(conn) + n := 30 + conds := test.AddConditions(ctx, t, snippetCfg.APIs.GRPC.Authorization.OwnerClaim, c, n, nil) + + type args struct { + token string + httpIDFilter []string + } + tests := []struct { + name string + args args + wantHTTPCode int + wantErr bool + want func(*testing.T, []*pb.Condition) + }{ + { + name: "missing owner", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: nil, + }), + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + { + name: "get certain condition", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + httpIDFilter: []string{ + test.ConditionID(1) + "/0", + }, + }, + wantHTTPCode: http.StatusOK, + want: func(t *testing.T, values []*pb.Condition) { + require.Len(t, values, 1) + require.Equal(t, test.ConditionID(1), values[0].GetId()) + require.Equal(t, uint64(0), values[0].GetVersion()) + }, + }, + { + name: "owner1/default", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + }, + wantHTTPCode: http.StatusOK, + want: func(t *testing.T, values []*pb.Condition) { + require.NotEmpty(t, values) + require.InDelta(t, test.RuntimeConfig.NumConditions/test.RuntimeConfig.NumOwners, len(values), 1) + for _, v := range values { + cond, ok := conds[v.GetId()] + require.True(t, ok) + test.ConditionContains(t, cond, v) + } + }, + }, + { + name: "owner1/all", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + httpIDFilter: []string{ + "//all", + }, + }, + wantHTTPCode: http.StatusOK, + want: func(t *testing.T, values []*pb.Condition) { + require.NotEmpty(t, values) + require.InDelta(t, n/test.RuntimeConfig.NumOwners, len(values), 1) + for _, v := range values { + cond, ok := conds[v.GetId()] + require.True(t, ok) + test.ConditionContains(t, cond, v) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rb := httpTest.NewRequest(http.MethodGet, test.HTTPURI(snippetHttp.Conditions), nil).AuthToken(tt.args.token) + rb = rb.Accept(pkgHttp.ApplicationProtoJsonContentType).ContentType(message.AppCBOR.String()) + if len(tt.args.httpIDFilter) > 0 { + rb = rb.AddQuery(uri.HTTPIDFilterQueryKey, tt.args.httpIDFilter...) + } + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + values := make([]*pb.Condition, 0, 1) + for { + var value pb.Condition + err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &value) + if errors.Is(err, io.EOF) { + break + } + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + values = append(values, &value) + } + tt.want(t, values) + }) + } +} diff --git a/snippet-service/service/http/getConfigurations_test.go b/snippet-service/service/http/getConfigurations_test.go new file mode 100644 index 000000000..6e819cb1c --- /dev/null +++ b/snippet-service/service/http/getConfigurations_test.go @@ -0,0 +1,163 @@ +package http_test + +import ( + "context" + "crypto/tls" + "errors" + "io" + "net/http" + "testing" + + "github.com/plgd-dev/go-coap/v3/message" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/snippet-service/uri" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestRequestHandlerGetConfigurations(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + defer shutDown() + + snippetCfg := test.MakeConfig(t) + _, shutdownHttp := test.New(t, snippetCfg) + defer shutdownHttp() + + conn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewSnippetServiceClient(conn) + n := 30 + confs := test.AddConfigurations(ctx, t, snippetCfg.APIs.GRPC.Authorization.OwnerClaim, c, n, nil) + + type args struct { + token string + uri string + httpIDFilter []string + } + tests := []struct { + name string + args args + wantHTTPCode int + wantErr bool + want func([]*pb.Configuration) + }{ + { + name: "missing owner", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: nil, + }), + uri: test.HTTPURI(snippetHttp.Configurations), + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + { + name: "owner1/id1/version0", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + uri: test.HTTPURI(snippetHttp.Configurations), + httpIDFilter: []string{ + test.ConfigurationID(1) + "/0", + }, + }, + wantHTTPCode: http.StatusOK, + want: func(values []*pb.Configuration) { + require.Len(t, values, 1) + require.Equal(t, test.ConfigurationID(1), values[0].GetId()) + require.Equal(t, uint64(0), values[0].GetVersion()) + }, + }, + { + name: "owner1/default", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + uri: test.HTTPURI(snippetHttp.Configurations), + }, + wantHTTPCode: http.StatusOK, + want: func(values []*pb.Configuration) { + require.NotEmpty(t, values) + require.InDelta(t, test.RuntimeConfig.NumConfigurations/test.RuntimeConfig.NumOwners, len(values), 1) + for _, v := range values { + conf, ok := confs[v.GetId()] + require.True(t, ok) + test.ConfigurationContains(t, conf, v) + } + }, + }, + { + name: "owner1/all", + args: args{ + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: test.Owner(1), + }), + uri: test.HTTPURI(snippetHttp.Configurations), + httpIDFilter: []string{ + "/all", + }, + }, + wantHTTPCode: http.StatusOK, + want: func(values []*pb.Configuration) { + require.NotEmpty(t, values) + require.InDelta(t, len(confs)*(n/len(confs)/test.RuntimeConfig.NumOwners), len(values), 1) + for _, v := range values { + conf, ok := confs[v.GetId()] + require.True(t, ok) + test.ConfigurationContains(t, conf, v) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rb := httpTest.NewRequest(http.MethodGet, tt.args.uri, nil).AuthToken(tt.args.token) + rb = rb.Accept(pkgHttp.ApplicationProtoJsonContentType).ContentType(message.AppCBOR.String()) + if len(tt.args.httpIDFilter) > 0 { + rb = rb.AddQuery(uri.HTTPIDFilterQueryKey, tt.args.httpIDFilter...) + } + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + values := make([]*pb.Configuration, 0, 1) + for { + var value pb.Configuration + err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &value) + if errors.Is(err, io.EOF) { + break + } + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + values = append(values, &value) + } + tt.want(values) + }) + } +} diff --git a/snippet-service/service/http/invokeConfiguration_test.go b/snippet-service/service/http/invokeConfiguration_test.go new file mode 100644 index 000000000..7d8f6db3f --- /dev/null +++ b/snippet-service/service/http/invokeConfiguration_test.go @@ -0,0 +1,414 @@ +package http_test + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "io" + "net/http" + "strconv" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/go-coap/v3/message" + grpcGwPb "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/snippet-service/updater" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func invokeConfiguration(ctx context.Context, t *testing.T, id, token string, req *pb.InvokeConfigurationRequest) (*pb.InvokeConfigurationResponse, int, error) { + data, err := httpTest.GetContentData(&grpcGwPb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, req), + }, message.AppJSON.String()) + if err != nil { + return nil, 0, err + } + + rb := httpTest.NewRequest(http.MethodPost, test.HTTPURI(snippetHttp.AliasConfigurations), bytes.NewReader(data)).AuthToken(token) + rb.Accept(pkgHttp.ApplicationProtoJsonContentType).ContentType(message.AppJSON.String()).ID(id) + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + + var got pb.InvokeConfigurationResponse + err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + return &got, resp.StatusCode, err +} + +func getPendingCommands(ctx context.Context, t *testing.T, c grpcGwPb.GrpcGatewayClient, deviceID string) map[string][]*grpcGwPb.PendingCommand { + pendingCommandsClient, err := c.GetPendingCommands(ctx, &grpcGwPb.GetPendingCommandsRequest{ + DeviceIdFilter: []string{deviceID}, + IncludeHiddenResources: true, + }) + require.NoError(t, err) + pendingCommands := make(map[string][]*grpcGwPb.PendingCommand) + for { + ev, err2 := pendingCommandsClient.Recv() + if errors.Is(err2, io.EOF) { + break + } + require.NoError(t, err2) + var id string + switch c := ev.GetCommand().(type) { + case *grpcGwPb.PendingCommand_DeviceMetadataUpdatePending: + id = c.DeviceMetadataUpdatePending.GetAuditContext().GetCorrelationId() + case *grpcGwPb.PendingCommand_ResourceCreatePending: + id = c.ResourceCreatePending.GetAuditContext().GetCorrelationId() + case *grpcGwPb.PendingCommand_ResourceDeletePending: + id = c.ResourceDeletePending.GetAuditContext().GetCorrelationId() + case *grpcGwPb.PendingCommand_ResourceRetrievePending: + id = c.ResourceRetrievePending.GetAuditContext().GetCorrelationId() + case *grpcGwPb.PendingCommand_ResourceUpdatePending: + id = c.ResourceUpdatePending.GetAuditContext().GetCorrelationId() + } + + appliedConfID, _, _, ok := updater.SplitCorrelationID(id) + if !ok { + continue + } + pendingCommands[appliedConfID] = append(pendingCommands[appliedConfID], ev) + } + return pendingCommands +} + +func TestRequestHandlerInvokeConfiguration(t *testing.T) { + deviceID := hubTest.MustFindDeviceByName(hubTest.TestDeviceName) + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + tearDown := service.SetUp(ctx, t) + defer tearDown() + + token := oauthTest.GetDefaultAccessToken(t) + ctxWithToken := pkgGrpc.CtxWithToken(ctx, token) + + grpcClient := grpcgwTest.NewTestClient(t) + defer func() { + errC := grpcClient.Close() + require.NoError(t, errC) + }() + resources := hubTest.GetAllBackendResourceLinks() + _, shutdownDevSim := hubTest.OnboardDevSim(ctxWithToken, t, grpcClient.GrpcGatewayClient(), deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, resources) + defer shutdownDevSim() + + defer func() { + // restore state + errU := grpcClient.UpdateResource(ctxWithToken, deviceID, hubTest.TestResourceLightInstanceHref("1"), map[string]interface{}{ + "state": false, + "power": uint64(0), + }, nil) + require.NoError(t, errU) + }() + + snippetCfg := test.MakeConfig(t) + snippetCfg.Clients.ResourceUpdater.CleanUpExpiredUpdates = "*/1 * * * * *" + snippetCfg.Clients.ResourceUpdater.ExtendCronParserBySeconds = true + _, shutdownHttp := test.New(t, snippetCfg) + defer shutdownHttp() + logger := log.NewLogger(snippetCfg.Log) + + conn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + snippetClient := pb.NewSnippetServiceClient(conn) + + notExistingResourceHref := "/not/existing" + canceledResourceHref := "/canceled" + // configuration1 + // -> /light/1 -> { state: on } + // -> /not/existing -> { value: 42 } + conf1, err := snippetClient.CreateConfiguration(ctxWithToken, &pb.Configuration{ + Name: "update", + Owner: oauthService.DeviceUserID, + Resources: []*pb.Configuration_Resource{ + { + Href: hubTest.TestResourceLightInstanceHref("1"), + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "state": true, + }), + }, + }, + { + Href: notExistingResourceHref, + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "value": 42, + }), + }, + TimeToLive: int64(100 * time.Millisecond), + }, + { + Href: canceledResourceHref, + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "level": "leet", + }), + }, + TimeToLive: int64(5 * time.Minute), + }, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, conf1.GetId()) + + // configuration -> /light/1 -> { power: 42 } + conf2, err := snippetClient.CreateConfiguration(ctxWithToken, &pb.Configuration{ + Name: "update light power", + Owner: oauthService.DeviceUserID, + Resources: []*pb.Configuration_Resource{ + { + Href: hubTest.TestResourceLightInstanceHref("1"), + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "power": 42, + }), + }, + TimeToLive: int64(500 * time.Millisecond), + }, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, conf2.GetId()) + + logger.Infof("fail invoke configuration: missing owner in token") + tokenWithoutOwner := oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: nil, + }) + _, code, err := invokeConfiguration(ctxWithToken, t, conf1.GetId(), tokenWithoutOwner, &pb.InvokeConfigurationRequest{ + ConfigurationId: conf1.GetId(), + DeviceId: deviceID, + CorrelationId: uuid.NewString(), + }) + require.Equal(t, http.StatusForbidden, code) + require.Error(t, err) + + logger.Infof("fail invoke configuration: missing deviceID") + _, code, err = invokeConfiguration(ctxWithToken, t, conf1.GetId(), token, &pb.InvokeConfigurationRequest{ + ConfigurationId: conf1.GetId(), + CorrelationId: uuid.NewString(), + }) + require.Equal(t, http.StatusInternalServerError, code) + require.Error(t, err) + + logger.Infof("invoke configuration(%v)", conf1.GetId()) + correlationID1 := uuid.NewString() + got, code, err := invokeConfiguration(ctxWithToken, t, conf1.GetId(), token, &pb.InvokeConfigurationRequest{ + ConfigurationId: conf1.GetId(), + DeviceId: deviceID, + CorrelationId: correlationID1, + }) + require.Equal(t, http.StatusOK, code) + require.NoError(t, err) + + light1Conf1ID := conf1.GetId() + "." + hubTest.TestResourceLightInstanceHref("1") + notExistingConf1ID := conf1.GetId() + "." + notExistingResourceHref + cancledConf1ID := conf1.GetId() + "." + canceledResourceHref + + appliedConfResources := test.WaitForAppliedConfigurations(ctxWithToken, t, snippetClient, + &pb.GetAppliedConfigurationsRequest{ + IdFilter: []string{got.GetAppliedConfigurationId()}, + }, + map[string][]pb.AppliedConfiguration_Resource_Status{ + hubTest.TestResourceLightInstanceHref("1"): {pb.AppliedConfiguration_Resource_DONE}, + notExistingResourceHref: {pb.AppliedConfiguration_Resource_TIMEOUT}, + canceledResourceHref: {pb.AppliedConfiguration_Resource_PENDING}, + }, + ) + require.NotEmpty(t, appliedConfResources) + + canceledConf1, ok := appliedConfResources[cancledConf1ID] + require.True(t, ok) + // the second invocation with force should cancel this resource update + require.Equal(t, pb.AppliedConfiguration_Resource_PENDING, canceledConf1.GetStatus()) + notExistingConf1, ok := appliedConfResources[notExistingConf1ID] + require.True(t, ok) + require.Equal(t, pb.AppliedConfiguration_Resource_TIMEOUT, notExistingConf1.GetStatus()) + require.Equal(t, commands.Status_ERROR, notExistingConf1.GetResourceUpdated().GetStatus()) + lightConf1, ok := appliedConfResources[light1Conf1ID] + require.True(t, ok) + require.Equal(t, pb.AppliedConfiguration_Resource_DONE, lightConf1.GetStatus()) + require.Equal(t, commands.Status_OK, lightConf1.GetResourceUpdated().GetStatus()) + + // /light/1 -> should be updated by invoked conf1 + var gotLight map[interface{}]interface{} + err = grpcClient.GetResource(ctxWithToken, deviceID, hubTest.TestResourceLightInstanceHref("1"), &gotLight) + require.NoError(t, err) + + require.Equal(t, map[interface{}]interface{}{ + "state": true, + "power": uint64(0), + "name": "Light", + }, gotLight) + + logger.Infof("duplicit invoke configuration(%v)", conf1.GetId()) + // duplicit invocation of the same configuration + correlationID2 := uuid.NewString() + _, code, err = invokeConfiguration(ctxWithToken, t, conf1.GetId(), token, &pb.InvokeConfigurationRequest{ + ConfigurationId: conf1.GetId(), + DeviceId: deviceID, + CorrelationId: correlationID2, + }) + require.Equal(t, http.StatusInternalServerError, code) + require.Error(t, err) + + logger.Infof("force invoke configuration(%v)", conf1.GetId()) + got2, code, err := invokeConfiguration(ctxWithToken, t, conf1.GetId(), token, &pb.InvokeConfigurationRequest{ + ConfigurationId: conf1.GetId(), + DeviceId: deviceID, + CorrelationId: correlationID2, + Force: true, + }) + require.Equal(t, http.StatusOK, code) + require.NoError(t, err) + require.NotEqual(t, got.GetAppliedConfigurationId(), got2.GetAppliedConfigurationId()) + + appliedConfResources = test.WaitForAppliedConfigurations(ctxWithToken, t, snippetClient, + &pb.GetAppliedConfigurationsRequest{ + IdFilter: []string{got2.GetAppliedConfigurationId()}, + }, + map[string][]pb.AppliedConfiguration_Resource_Status{ + hubTest.TestResourceLightInstanceHref("1"): {pb.AppliedConfiguration_Resource_DONE}, + notExistingResourceHref: {pb.AppliedConfiguration_Resource_TIMEOUT}, + canceledResourceHref: {pb.AppliedConfiguration_Resource_PENDING}, + }, + ) + require.NotEmpty(t, appliedConfResources) + + notExistingConf1, ok = appliedConfResources[notExistingConf1ID] + require.True(t, ok) + require.Equal(t, pb.AppliedConfiguration_Resource_TIMEOUT, notExistingConf1.GetStatus()) + require.Equal(t, commands.Status_ERROR, notExistingConf1.GetResourceUpdated().GetStatus()) + lightConf1, ok = appliedConfResources[light1Conf1ID] + require.True(t, ok) + require.Equal(t, pb.AppliedConfiguration_Resource_DONE, lightConf1.GetStatus()) + require.Equal(t, commands.Status_OK, lightConf1.GetResourceUpdated().GetStatus()) + + appliedConfs, _ := test.GetAppliedConfigurations(ctxWithToken, t, snippetClient, &pb.GetAppliedConfigurationsRequest{ + DeviceIdFilter: []string{deviceID}, + }) + require.Len(t, appliedConfs, 1) +} + +func TestParallelRequestHandlerInvokeConfiguration(t *testing.T) { + deviceID := hubTest.MustFindDeviceByName(hubTest.TestDeviceName) + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + tearDown := service.SetUp(ctx, t) + defer tearDown() + + token := oauthTest.GetDefaultAccessToken(t) + ctxWithToken := pkgGrpc.CtxWithToken(ctx, token) + + grpcClient := grpcgwTest.NewTestClient(t) + defer func() { + errC := grpcClient.Close() + require.NoError(t, errC) + }() + _, shutdownDevSim := hubTest.OnboardDevSim(ctxWithToken, t, grpcClient.GrpcGatewayClient(), deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, hubTest.GetAllBackendResourceLinks()) + defer shutdownDevSim() + + snippetCfg := test.MakeConfig(t) + _, shutdownHttp := test.New(t, snippetCfg) + defer shutdownHttp() + + conn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + snippetClient := pb.NewSnippetServiceClient(conn) + + // multiple resources, so the iteration is more time consuming and parallel invocations should occur + resourcesCount := 10 + resources := make([]*pb.Configuration_Resource, 0, resourcesCount) + for i := range resourcesCount { + href := "/not/existing/" + strconv.Itoa(i+1) + resources = append(resources, &pb.Configuration_Resource{ + Href: href, + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "value": i, + }), + }, + }) + } + + conf, err := snippetClient.CreateConfiguration(ctxWithToken, &pb.Configuration{ + Owner: oauthService.DeviceUserID, + Resources: resources, + }) + require.NoError(t, err) + + type invocationResult struct { + appliedConfID string + code int + err error + } + parallelCount := 3 + results := make([]invocationResult, parallelCount) + wg := sync.WaitGroup{} + wg.Add(parallelCount) + for i := range parallelCount { + go func(id int) { + defer wg.Done() + got, code, err := invokeConfiguration(ctxWithToken, t, conf.GetId(), token, &pb.InvokeConfigurationRequest{ + ConfigurationId: conf.GetId(), + DeviceId: deviceID, + Force: true, + }) + appliedConfID := got.GetAppliedConfigurationId() + results[id] = invocationResult{appliedConfID: appliedConfID, code: code, err: err} + }(i) + } + wg.Wait() + + pendingCommands := getPendingCommands(ctxWithToken, t, grpcClient.GrpcGatewayClient(), deviceID) + // only pending commands for the last applied configuration should be present, all others should be canceled + require.Len(t, pendingCommands, 1) + key := maps.Keys(pendingCommands)[0] + for _, r := range results { + if r.appliedConfID != key { + require.Equal(t, http.StatusInternalServerError, r.code) + require.Error(t, r.err) + continue + } + require.Equal(t, http.StatusOK, r.code) + require.NoError(t, r.err) + require.Len(t, pendingCommands[key], resourcesCount) + } +} diff --git a/snippet-service/service/http/requestHandler.go b/snippet-service/service/http/requestHandler.go new file mode 100644 index 000000000..acb138acb --- /dev/null +++ b/snippet-service/service/http/requestHandler.go @@ -0,0 +1,79 @@ +package http + +import ( + "context" + "fmt" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/plgd-dev/hub/v2/http-gateway/serverMux" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + grpcService "github.com/plgd-dev/hub/v2/snippet-service/service/grpc" +) + +// RequestHandler for handling incoming request +type RequestHandler struct { + config *Config + mux *runtime.ServeMux +} + +/* +// TODO: HTTP-API: /configurations/123?version=latest +// - the GRPC query parser doesn't seem to support oneOf fields, so we have to manually encode and decode the query +func (requestHandler *RequestHandler) getConfigurationVersion(w http.ResponseWriter, r *http.Request) { + // /api/v1/configuration/{id}?version=latest -> rpc GetConfigurations + IDFilter{IDFilter_Latest} + // /api/v1/configuration/{id}?version=all -> rpc GetConfigurations + IDFilter{IDFilter_All} + // /api/v1/configuration/{id}?version={version} -> rpc GetConfigurations + IDFilter{IDFilter_Version{version}} + vars := mux.Vars(r) + configurationID := vars[IDKey] + + versionStr := r.URL.Query().Get(VersionQueryKey) + if versionStr != "" && versionStr != "all" && versionStr == "latest" { + var err error + _, err = strconv.ParseUint(versionStr, 10, 64) + if err != nil { + serverMux.WriteError(w, fmt.Errorf("invalid configuration('%v') version: %w", configurationID, err)) + return + } + } + + type Options struct { + HTTPIDFilter []string `url:"httpIdFilter"` + } + opt := &Options{ + HTTPIDFilter: []string{configurationID + "/" + versionStr}, + } + q, err := query.Values(opt) + if err != nil { + serverMux.WriteError(w, fmt.Errorf("invalid configuration('%v') version: %w", configurationID, err)) + return + } + + r.URL.Path = Configurations + r.URL.RawQuery = q.Encode() + requestHandler.mux.ServeHTTP(w, r) +} +*/ + +// NewHTTP returns HTTP handler +func NewRequestHandler(config *Config, r *mux.Router, snippetServiceServer *grpcService.SnippetServiceServer) (*RequestHandler, error) { + requestHandler := &RequestHandler{ + config: config, + mux: serverMux.New(), + } + + // Aliases + // r.HandleFunc(AliasConfigurations, requestHandler.getConfigurationVersion).Methods(http.MethodGet) + + ch := new(inprocgrpc.Channel) + pb.RegisterSnippetServiceServer(ch, snippetServiceServer) + grpcClient := pb.NewSnippetServiceClient(ch) + // register grpc-proxy handler + if err := pb.RegisterSnippetServiceHandlerClient(context.Background(), requestHandler.mux, grpcClient); err != nil { + return nil, fmt.Errorf("failed to register snippet-service handler: %w", err) + } + r.PathPrefix("/").Handler(requestHandler.mux) + + return requestHandler, nil +} diff --git a/snippet-service/service/http/service.go b/snippet-service/service/http/service.go new file mode 100644 index 000000000..d9ca71cae --- /dev/null +++ b/snippet-service/service/http/service.go @@ -0,0 +1,48 @@ +package http + +import ( + "fmt" + + "github.com/plgd-dev/hub/v2/http-gateway/uri" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + httpService "github.com/plgd-dev/hub/v2/pkg/net/http/service" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" + grpcService "github.com/plgd-dev/hub/v2/snippet-service/service/grpc" + "go.opentelemetry.io/otel/trace" +) + +// Service handle HTTP request +type Service struct { + *httpService.Service + requestHandler *RequestHandler +} + +// New parses configuration and creates new Server with provided store and bus +func New(serviceName string, config Config, snippetServiceServer *grpcService.SnippetServiceServer, validator *validator.Validator, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Service, error) { + service, err := httpService.New(httpService.Config{ + HTTPConnection: config.Connection, + HTTPServer: config.Server, + ServiceName: serviceName, + AuthRules: pkgHttp.NewDefaultAuthorizationRules(uri.API), + FileWatcher: fileWatcher, + Logger: logger, + TraceProvider: tracerProvider, + Validator: validator, + }) + if err != nil { + return nil, fmt.Errorf("cannot create http service: %w", err) + } + + requestHandler, err := NewRequestHandler(&config, service.GetRouter(), snippetServiceServer) + if err != nil { + _ = service.Close() + return nil, err + } + + return &Service{ + Service: service, + requestHandler: requestHandler, + }, nil +} diff --git a/snippet-service/service/http/updateCondition_test.go b/snippet-service/service/http/updateCondition_test.go new file mode 100644 index 000000000..2febb603f --- /dev/null +++ b/snippet-service/service/http/updateCondition_test.go @@ -0,0 +1,190 @@ +package http_test + +import ( + "bytes" + "context" + "crypto/tls" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + snippetPb "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + snippetTest "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func makeUpdateCondition(id, configurationID, owner string, i int) *snippetPb.Condition { + return &snippetPb.Condition{ + Id: id, + ConfigurationId: configurationID, + Owner: owner, + Version: uint64(i), + Name: snippetTest.ConditionName(i), + Enabled: true, + DeviceIdFilter: snippetTest.ConditionDeviceIdFilter(i+1, i+1), + ResourceTypeFilter: snippetTest.ConditionResourceTypeFilter(i+1, i+1), + ResourceHrefFilter: snippetTest.ConditionResourceHrefFilter(i+1, i+1), + JqExpressionFilter: snippetTest.ConditionJqExpressionFilter(i), + ApiAccessToken: snippetTest.ConditionApiAccessToken(i), + } +} + +func TestRequestHandlerUpdateCondition(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + defer shutDown() + + _, shutdownHttp := snippetTest.SetUp(t) + defer shutdownHttp() + + token := oauthTest.GetDefaultAccessToken(t) + + conn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := snippetPb.NewSnippetServiceClient(conn) + + cond := makeUpdateCondition(uuid.NewString(), uuid.NewString(), oauthService.DeviceUserID, 0) + _, err = c.CreateCondition(pkgGrpc.CtxWithToken(ctx, token), cond) + require.NoError(t, err) + + type args struct { + id string + cond *snippetPb.Condition + token string + } + tests := []struct { + name string + args args + want *snippetPb.Condition + wantHTTPCode int + wantErr bool + }{ + { + name: "invalid Id", + args: args{ + id: "invalid", + cond: func() *snippetPb.Condition { + c := makeUpdateCondition(cond.GetId(), cond.GetConfigurationId(), cond.GetOwner(), 1) + c.Id = "invalid" + return c + }(), + token: token, + }, + wantHTTPCode: http.StatusBadRequest, + wantErr: true, + }, + { + name: "invalid configurationID", + args: args{ + id: cond.GetId(), + cond: func() *snippetPb.Condition { + c := makeUpdateCondition(cond.GetId(), cond.GetConfigurationId(), cond.GetOwner(), 1) + c.ConfigurationId = "invalid" + return c + }(), + token: token, + }, + wantHTTPCode: http.StatusBadRequest, + wantErr: true, + }, + { + name: "configurationID mismatch", + args: args{ + id: cond.GetId(), + cond: makeUpdateCondition(cond.GetId(), uuid.NewString(), oauthService.DeviceUserID, 1), + token: token, + }, + wantHTTPCode: http.StatusInternalServerError, + wantErr: true, + }, + { + name: "non-matching owner", + args: args{ + id: cond.GetId(), + cond: makeUpdateCondition(cond.GetId(), uuid.NewString(), "non-matching owner", 1), + token: token, + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + { + name: "update", + args: args{ + id: cond.GetId(), + cond: makeUpdateCondition(cond.GetId(), "", "", 1), + token: token, + }, + wantHTTPCode: http.StatusOK, + want: makeUpdateCondition(cond.GetId(), cond.GetConfigurationId(), oauthService.DeviceUserID, 1), + }, + { + name: "update (with owner)", + args: args{ + id: cond.GetId(), + cond: makeUpdateCondition(cond.GetId(), cond.GetConfigurationId(), oauthService.DeviceUserID, 2), + token: token, + }, + wantHTTPCode: http.StatusOK, + want: makeUpdateCondition(cond.GetId(), cond.GetConfigurationId(), oauthService.DeviceUserID, 2), + }, + { + name: "update (with overwritten ID)", + args: args{ + id: cond.GetId(), + // the ID will get overwritten by the ID in the query + cond: makeUpdateCondition(uuid.NewString(), "", oauthService.DeviceUserID, 3), + token: token, + }, + wantHTTPCode: http.StatusOK, + want: makeUpdateCondition(cond.GetId(), cond.GetConfigurationId(), oauthService.DeviceUserID, 3), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := httpTest.GetContentData(&pb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, tt.args.cond), + }, message.AppJSON.String()) + require.NoError(t, err) + + rb := httpTest.NewRequest(http.MethodPut, snippetTest.HTTPURI(snippetHttp.AliasConditions), bytes.NewReader(data)).AuthToken(tt.args.token) + rb.Accept(pkgHttp.ApplicationProtoJsonContentType).ContentType(message.AppJSON.String()).ID(tt.args.id) + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + var got snippetPb.Condition + err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + snippetTest.CmpCondition(t, tt.want, &got, true) + }) + } +} diff --git a/snippet-service/service/http/updateConfiguration_test.go b/snippet-service/service/http/updateConfiguration_test.go new file mode 100644 index 000000000..b44d5a84e --- /dev/null +++ b/snippet-service/service/http/updateConfiguration_test.go @@ -0,0 +1,194 @@ +package http_test + +import ( + "bytes" + "context" + "crypto/tls" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + snippetPb "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + snippetTest "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestRequestHandlerUpdateConfiguration(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + defer shutDown() + + _, shutdownHttp := snippetTest.SetUp(t) + defer shutdownHttp() + + conn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := snippetPb.NewSnippetServiceClient(conn) + conf := &snippetPb.Configuration{ + Id: uuid.NewString(), + Version: 0, + Name: "configurationToUpdate", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/1", 1), + }, + } + token := oauthTest.GetDefaultAccessToken(t) + _, err = c.CreateConfiguration(pkgGrpc.CtxWithToken(ctx, token), conf) + require.NoError(t, err) + + type args struct { + id string + conf *snippetPb.Configuration + token string + } + tests := []struct { + name string + args args + wantHTTPCode int + wantErr bool + }{ + { + name: "invalid ID", + args: args{ + id: "invalid", + conf: &snippetPb.Configuration{ + Version: 42, + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/1", 42), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusBadRequest, + wantErr: true, + }, + { + name: "missing resources", + args: args{ + id: conf.GetId(), + conf: &snippetPb.Configuration{ + Version: 42, + }, + token: token, + }, + wantHTTPCode: http.StatusBadRequest, + wantErr: true, + }, + { + name: "non-matching owner", + args: args{ + id: conf.GetId(), + conf: &snippetPb.Configuration{ + Version: 42, + Owner: "non-matching owner", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/2", 52), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + { + name: "update", + args: args{ + id: conf.GetId(), + conf: &snippetPb.Configuration{ + Version: 1, + Name: "updated1", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/3", 62), + makeTestResource(t, "/test/4", 72), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusOK, + }, + { + name: "update (with owner)", + args: args{ + id: conf.GetId(), + conf: &snippetPb.Configuration{ + Version: 2, + Owner: oauthService.DeviceUserID, + Name: "updated2", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/5", 82), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusOK, + }, + { + name: "update (with overwritten ID)", + args: args{ + id: conf.GetId(), + conf: &snippetPb.Configuration{ + Id: uuid.NewString(), // this ID will get overwritten by the ID in the query + Version: 3, + Name: "updated3", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/6", 92), + makeTestResource(t, "/test/7", 102), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusOK, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := httpTest.GetContentData(&pb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, tt.args.conf), + }, message.AppJSON.String()) + require.NoError(t, err) + + rb := httpTest.NewRequest(http.MethodPut, snippetTest.HTTPURI(snippetHttp.AliasConfigurations), bytes.NewReader(data)).AuthToken(tt.args.token) + rb.Accept(pkgHttp.ApplicationProtoJsonContentType).ContentType(message.AppJSON.String()).ID(tt.args.id) + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + var got snippetPb.Configuration + err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + want := tt.args.conf + want.Id = tt.args.id + want.Owner = oauthService.DeviceUserID + snippetTest.CmpConfiguration(t, want, &got, true) + }) + } +} diff --git a/snippet-service/service/http/uri.go b/snippet-service/service/http/uri.go new file mode 100644 index 000000000..8ca76d0c8 --- /dev/null +++ b/snippet-service/service/http/uri.go @@ -0,0 +1,34 @@ +package http + +const ( + IDKey = "id" + ConfigurationIDKey = "configurationId" + + VersionQueryKey = "version" + + API string = "/snippet-service/api/v1" + + // GET /snippet-service/api/v1/conditions -> rpc GetConditions + // DELETE /snippet-service/api/v1/conditions -> rpc DeleteConditions + // POST /snippet-service/api/v1/conditions -> rpc CreateCondition + Conditions = API + "/conditions" + + // PUT /snippet-service/api/v1/conditions/{id} -> rpc UpdateCondition + AliasConditions = Conditions + "/{" + IDKey + "}" + + // GET /snippet-service/api/v1/configurations -> rpc GetConfigurations + // DELETE /snippet-service/api/v1/configurations -> rpc DeleteConfigurations + // POST /snippet-service/api/v1/configurations -> rpc CreateConfiguration + Configurations = API + "/configurations" + + // POST /snippet-service/api/v1/configurations/{id} -> rpc InvokeConfiguration + // PUT /snippet-service/api/v1/configurations/{id} -> rpc UpdateConfiguration + // GET /snippet-service/api/v1/configurations/{id}?version=latest -> rpc GetConfigurations + IDFilter{IDFilter_Latest} + // GET /snippet-service/api/v1/configurations/{id}?version=all -> rpc GetConfigurations + IDFilter{IDFilter_All} + // GET /snippet-service/api/v1/configurations/{id}?version={version} -> rpc GetConfigurations + IDFilter{IDFilter_Version{version}} + AliasConfigurations = Configurations + "/{" + IDKey + "}" + + // GET /snippet-service/api/v1/configurations/applied -> rpc GetAppliedConfigurations + // DELETE /snippet-service/api/v1/configurations/applied -> rpc DeleteAppliedConfigurations + AppliedConfigurations = Configurations + "/applied" +) diff --git a/snippet-service/service/resourceSubscriber.go b/snippet-service/service/resourceSubscriber.go new file mode 100644 index 000000000..5c7a34c31 --- /dev/null +++ b/snippet-service/service/resourceSubscriber.go @@ -0,0 +1,75 @@ +package service + +import ( + "context" + "fmt" + + isEvents "github.com/plgd-dev/hub/v2/identity-store/events" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus" + natsClient "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus/nats/client" + "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus/nats/subscriber" + "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/utils" + "github.com/plgd-dev/hub/v2/resource-aggregate/events" +) + +type ResourceSubscriber struct { + natsClient *natsClient.Client + subscriptionHandler eventbus.Handler + subscriber *subscriber.Subscriber + observer eventbus.Observer +} + +func WithAllDevicesAndResources() func(values map[string]string) { + return func(values map[string]string) { + values[utils.DeviceIDKey] = "*" + values[utils.HrefIDKey] = "*" + } +} + +func NewResourceSubscriber(ctx context.Context, config natsClient.Config, subscriptionID string, fileWatcher *fsnotify.Watcher, logger log.Logger, handler eventbus.Handler) (*ResourceSubscriber, error) { + nats, err := natsClient.New(config, fileWatcher, logger) + if err != nil { + return nil, fmt.Errorf("cannot create nats client: %w", err) + } + + subscriber, err := subscriber.New(nats.GetConn(), + config.PendingLimits, + logger, + subscriber.WithUnmarshaler(utils.Unmarshal)) + if err != nil { + nats.Close() + return nil, fmt.Errorf("cannot create resource subscriber: %w", err) + } + + const owner = "*" + subjectResourceChanged := isEvents.ToSubject(utils.PlgdOwnersOwnerDevicesDeviceResourcesResourceEvent, + isEvents.WithOwner(owner), + WithAllDevicesAndResources(), + isEvents.WithEventType((&events.ResourceChanged{}).EventType())) + subjectResourceUpdated := isEvents.ToSubject(utils.PlgdOwnersOwnerDevicesDeviceResourcesResourceEvent, + isEvents.WithOwner(owner), + WithAllDevicesAndResources(), + isEvents.WithEventType((&events.ResourceUpdated{}).EventType())) + observer, err := subscriber.Subscribe(ctx, subscriptionID, []string{subjectResourceChanged, subjectResourceUpdated}, handler) + if err != nil { + subscriber.Close() + nats.Close() + return nil, fmt.Errorf("cannot subscribe to resource change events: %w", err) + } + + return &ResourceSubscriber{ + natsClient: nats, + subscriptionHandler: handler, + subscriber: subscriber, + observer: observer, + }, nil +} + +func (r *ResourceSubscriber) Close() error { + err := r.observer.Close() + r.subscriber.Close() + r.natsClient.Close() + return err +} diff --git a/snippet-service/service/resourceSubscriber_test.go b/snippet-service/service/resourceSubscriber_test.go new file mode 100644 index 000000000..f862209d8 --- /dev/null +++ b/snippet-service/service/resourceSubscriber_test.go @@ -0,0 +1,101 @@ +package service_test + +import ( + "context" + "crypto/tls" + "testing" + "time" + + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus" + "github.com/plgd-dev/hub/v2/resource-aggregate/events" + "github.com/plgd-dev/hub/v2/snippet-service/service" + "github.com/plgd-dev/hub/v2/snippet-service/test" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +type testHandler struct { + ch chan *events.ResourceChanged +} + +func (h *testHandler) Handle(ctx context.Context, iter eventbus.Iter) (err error) { + for { + ev, ok := iter.Next(ctx) + if !ok { + return iter.Err() + } + var s events.ResourceChanged + if ev.EventType() != s.EventType() { + continue + } + if err := ev.Unmarshal(&s); err != nil { + return err + } + h.ch <- &s + } +} + +func TestResourceSubscriber(t *testing.T) { + deviceID := hubTest.MustFindDeviceByName(hubTest.TestDeviceName) + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + tearDown := hubTestService.SetUp(ctx, t) + defer tearDown() + + logger := log.NewLogger(log.MakeDefaultConfig()) + fileWatcher, err := fsnotify.NewWatcher(logger) + require.NoError(t, err) + defer func() { + err = fileWatcher.Close() + require.NoError(t, err) + }() + h := testHandler{ + ch: make(chan *events.ResourceChanged, 8), + } + cfg := test.MakeConfig(t) + rs, err := service.NewResourceSubscriber(ctx, cfg.Clients.EventBus.NATS, cfg.Clients.EventBus.SubscriptionID, fileWatcher, logger, &h) + require.NoError(t, err) + defer rs.Close() + + ctx = pkgGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewGrpcGatewayClient(conn) + + resources := hubTest.GetAllBackendResourceLinks() + _, shutdownDevSim := hubTest.OnboardDevSim(ctx, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, resources) + defer shutdownDevSim() + + events := make(map[string]*events.ResourceChanged) + stop := false + for !stop { + select { + case ev := <-h.ch: + id := ev.GetResourceId().GetDeviceId() + ":" + ev.GetResourceId().GetHref() + events[id] = ev + case <-time.After(time.Second * 3): + stop = true + break + case <-ctx.Done(): + require.Fail(t, "timeout") + } + } + + require.Len(t, events, len(resources)) +} diff --git a/snippet-service/service/service.go b/snippet-service/service/service.go new file mode 100644 index 000000000..93ec56529 --- /dev/null +++ b/snippet-service/service/service.go @@ -0,0 +1,158 @@ +package service + +import ( + "context" + "fmt" + + "github.com/plgd-dev/hub/v2/pkg/config/database" + "github.com/plgd-dev/hub/v2/pkg/fn" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/net/listener" + otelClient "github.com/plgd-dev/hub/v2/pkg/opentelemetry/collector/client" + certManagerServer "github.com/plgd-dev/hub/v2/pkg/security/certManager/server" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" + "github.com/plgd-dev/hub/v2/pkg/service" + grpcService "github.com/plgd-dev/hub/v2/snippet-service/service/grpc" + httpService "github.com/plgd-dev/hub/v2/snippet-service/service/http" + "github.com/plgd-dev/hub/v2/snippet-service/store" + storeConfig "github.com/plgd-dev/hub/v2/snippet-service/store/config" + "github.com/plgd-dev/hub/v2/snippet-service/store/mongodb" + "github.com/plgd-dev/hub/v2/snippet-service/updater" + "go.opentelemetry.io/otel/trace" +) + +const serviceName = "snippet-service" + +type Service struct { + *service.Service + + store store.Store + resourceUpdater *updater.ResourceUpdater + resourceSubscriber *ResourceSubscriber +} + +func createStore(ctx context.Context, config storeConfig.Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (store.Store, error) { + if config.Use == database.MongoDB { + s, err := mongodb.New(ctx, config.MongoDB, fileWatcher, logger, tracerProvider) + if err != nil { + return nil, fmt.Errorf("mongodb: %w", err) + } + return s, nil + } + return nil, fmt.Errorf("invalid store use('%v')", config.Use) +} + +func newHttpService(ctx context.Context, config HTTPConfig, validatorConfig validator.Config, tlsConfig certManagerServer.Config, ss *grpcService.SnippetServiceServer, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*httpService.Service, func(), error) { + httpValidator, err := validator.New(ctx, validatorConfig, fileWatcher, logger, tracerProvider) + if err != nil { + return nil, nil, fmt.Errorf("cannot create http validator: %w", err) + } + httpService, err := httpService.New(serviceName, httpService.Config{ + Connection: listener.Config{ + Addr: config.Addr, + TLS: tlsConfig, + }, + Authorization: validatorConfig, + Server: config.Server, + }, ss, httpValidator, fileWatcher, logger, tracerProvider) + if err != nil { + httpValidator.Close() + return nil, nil, fmt.Errorf("cannot create http service: %w", err) + } + return httpService, httpValidator.Close, nil +} + +func newGrpcService(ctx context.Context, config grpcService.Config, ss *grpcService.SnippetServiceServer, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*grpcService.Service, func(), error) { + grpcValidator, err := validator.New(ctx, config.Authorization.Config, fileWatcher, logger, tracerProvider) + if err != nil { + return nil, nil, fmt.Errorf("cannot create grpc validator: %w", err) + } + grpcService, err := grpcService.New(config, ss, grpcValidator, fileWatcher, logger, tracerProvider) + if err != nil { + grpcValidator.Close() + return nil, nil, fmt.Errorf("cannot create grpc service: %w", err) + } + return grpcService, grpcValidator.Close, nil +} + +func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logger log.Logger) (*Service, error) { + otelClient, err := otelClient.New(ctx, config.Clients.OpenTelemetryCollector, serviceName, fileWatcher, logger) + if err != nil { + return nil, fmt.Errorf("cannot create open telemetry collector client: %w", err) + } + var closerFn fn.FuncList + closerFn.AddFunc(otelClient.Close) + tracerProvider := otelClient.GetTracerProvider() + + db, err := createStore(ctx, config.Clients.Storage, fileWatcher, logger, tracerProvider) + if err != nil { + closerFn.Execute() + return nil, fmt.Errorf("cannot create store: %w", err) + } + closerFn.AddFunc(func() { + if errC := db.Close(ctx); errC != nil { + log.Errorf("failed to close store: %w", errC) + } + }) + + resourceUpdater, err := updater.NewResourceUpdater(ctx, config.Clients.ResourceUpdater, db, fileWatcher, logger, tracerProvider) + if err != nil { + closerFn.Execute() + return nil, fmt.Errorf("cannot create resource change handler: %w", err) + } + closerFn.AddFunc(func() { + errC := resourceUpdater.Close() + if errC != nil { + log.Errorf("failed to close resource change handler: %w", errC) + } + }) + + resourceSubscriber, err := NewResourceSubscriber(ctx, config.Clients.EventBus.NATS, config.Clients.EventBus.SubscriptionID, fileWatcher, logger, resourceUpdater) + if err != nil { + closerFn.Execute() + return nil, fmt.Errorf("cannot create resource subscriber: %w", err) + } + closerFn.AddFunc(func() { + errC := resourceSubscriber.Close() + if errC != nil { + log.Errorf("failed to close resource subscriber: %w", errC) + } + }) + + snippetService := grpcService.NewSnippetServiceServer(db, resourceUpdater, config.APIs.GRPC.Authorization.OwnerClaim, config.HubID, logger) + + grpcService, grpcServiceClose, err := newGrpcService(ctx, config.APIs.GRPC, snippetService, fileWatcher, logger, tracerProvider) + if err != nil { + closerFn.Execute() + return nil, err + } + closerFn.AddFunc(grpcServiceClose) + + httpService, httpServiceClose, err := newHttpService(ctx, config.APIs.HTTP, config.APIs.GRPC.Authorization.Config, config.APIs.GRPC.TLS, + snippetService, fileWatcher, logger, tracerProvider) + if err != nil { + grpcService.Close() + closerFn.Execute() + return nil, err + } + closerFn.AddFunc(httpServiceClose) + + s := service.New(grpcService, httpService) + s.AddCloseFunc(closerFn.Execute) + return &Service{ + Service: s, + + store: db, + resourceUpdater: resourceUpdater, + resourceSubscriber: resourceSubscriber, + }, nil +} + +func (s *Service) SnippetServiceStore() store.Store { + return s.store +} + +func (s *Service) CancelPendingResourceUpdates(ctx context.Context) error { + return s.resourceUpdater.CancelPendingResourceUpdates(ctx) +} diff --git a/snippet-service/service/service_test.go b/snippet-service/service/service_test.go new file mode 100644 index 000000000..50ee558a3 --- /dev/null +++ b/snippet-service/service/service_test.go @@ -0,0 +1,631 @@ +package service_test + +import ( + "context" + "crypto/tls" + "fmt" + "testing" + "time" + + deviceClient "github.com/plgd-dev/device/v2/client" + "github.com/plgd-dev/device/v2/client/core" + bridgeDevice "github.com/plgd-dev/device/v2/cmd/bridge-device/device" + deviceCoap "github.com/plgd-dev/device/v2/pkg/net/coap" + "github.com/plgd-dev/device/v2/schema/configuration" + "github.com/plgd-dev/go-coap/v3/message" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" + "github.com/plgd-dev/hub/v2/pkg/config/database" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/mongodb" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/pkg/net/grpc/server" + otelClient "github.com/plgd-dev/hub/v2/pkg/opentelemetry/collector/client" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + natsClient "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus/nats/client" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/service" + storeConfig "github.com/plgd-dev/hub/v2/snippet-service/store/config" + storeCqlDB "github.com/plgd-dev/hub/v2/snippet-service/store/cqldb" + storeMongo "github.com/plgd-dev/hub/v2/snippet-service/store/mongodb" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/snippet-service/updater" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/plgd-dev/hub/v2/test/device/bridge" + oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/sdk" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestServiceNew(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + logger := log.NewLogger(log.MakeDefaultConfig()) + fileWatcher, err := fsnotify.NewWatcher(logger) + require.NoError(t, err) + + const services = hubTestService.SetUpServicesOAuth + tearDown := hubTestService.SetUpServices(ctx, t, services) + defer tearDown() + + tests := []struct { + name string + cfg service.Config + wantErr bool + }{ + { + name: "invalid open telemetry config", + cfg: service.Config{ + Clients: service.ClientsConfig{ + OpenTelemetryCollector: otelClient.Config{ + GRPC: otelClient.GRPCConfig{ + Enabled: true, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "invalid DB", + cfg: service.Config{ + Clients: service.ClientsConfig{ + Storage: storeConfig.Config{ + Use: "invalid", + }, + }, + }, + wantErr: true, + }, + { + name: "invalid mongoDB config", + cfg: service.Config{ + Clients: service.ClientsConfig{ + Storage: storeConfig.Config{ + Use: database.MongoDB, + MongoDB: &storeMongo.Config{ + Mongo: mongodb.Config{ + URI: "invalid", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "invalid cqlDB config", + cfg: service.Config{ + Clients: service.ClientsConfig{ + Storage: storeConfig.Config{ + Use: database.CqlDB, + CqlDB: &storeCqlDB.Config{}, + }, + }, + }, + wantErr: true, + }, + { + name: "invalid resource subscriber config", + cfg: func() service.Config { + cfg := test.MakeConfig(t) + cfg.Clients.EventBus.NATS = natsClient.Config{} + return cfg + }(), + wantErr: true, + }, + { + name: "invalid resource aggregate client config", + cfg: func() service.Config { + cfg := test.MakeConfig(t) + cfg.Clients.ResourceUpdater = updater.ResourceUpdaterConfig{} + return cfg + }(), + wantErr: true, + }, + { + name: "invalid GRPC validator config", + cfg: func() service.Config { + cfg := test.MakeConfig(t) + cfg.APIs.GRPC.Authorization = server.AuthorizationConfig{} + return cfg + }(), + wantErr: true, + }, + { + name: "invalid GRPC config", + cfg: func() service.Config { + cfg := test.MakeConfig(t) + cfg.APIs.GRPC.Addr = "invalid" + return cfg + }(), + wantErr: true, + }, + { + name: "invalid HTTP config", + cfg: func() service.Config { + cfg := test.MakeConfig(t) + cfg.APIs.HTTP.Addr = "invalid" + return cfg + }(), + wantErr: true, + }, + { + name: "valid", + cfg: test.MakeConfig(t), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, err := service.New(ctx, tt.cfg, fileWatcher, logger) + if tt.wantErr { + require.Error(t, err) + return + } + fmt.Printf("cfg: %v\n", tt.cfg) + require.NoError(t, err) + _ = s.Close() + }) + } +} + +func TestService(t *testing.T) { + deviceID := hubTest.MustFindDeviceByName(hubTest.TestDeviceName) + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + tearDown := hubTestService.SetUp(ctx, t) + defer tearDown() + + snippetCfg := test.MakeConfig(t) + const interval = time.Second + snippetCfg.Clients.ResourceUpdater.CleanUpExpiredUpdates = "*/1 * * * * *" + snippetCfg.Clients.ResourceUpdater.ExtendCronParserBySeconds = true + _, shutdownSnippetService := test.New(t, snippetCfg) + defer shutdownSnippetService() + + snippetClientConn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = snippetClientConn.Close() + }() + snippetClient := pb.NewSnippetServiceClient(snippetClientConn) + + token := oauthTest.GetDefaultAccessToken(t) + ctx = pkgGrpc.CtxWithToken(ctx, token) + + notExistingResourceHref := "/not/existing" + // configuration1 + // -> /light/1 -> { state: on } + // -> /not/existing -> { value: 42 } + conf1, err := snippetClient.CreateConfiguration(ctx, &pb.Configuration{ + Name: "update", + Owner: oauthService.DeviceUserID, + Resources: []*pb.Configuration_Resource{ + { + Href: hubTest.TestResourceLightInstanceHref("1"), + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "state": true, + }), + }, + }, + { + Href: notExistingResourceHref, + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "value": 42, + }), + }, + TimeToLive: int64(100 * time.Millisecond), + }, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, conf1.GetId()) + + // configuration2 -> /light/1 -> { power: 42 } + conf2, err := snippetClient.CreateConfiguration(ctx, &pb.Configuration{ + Name: "update light power", + Owner: oauthService.DeviceUserID, + Resources: []*pb.Configuration_Resource{ + { + Href: hubTest.TestResourceLightInstanceHref("1"), + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "power": 42, + }), + }, + TimeToLive: int64(500 * time.Millisecond), + }, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, conf2.GetId()) + + // configuration3 -> /oc/con-> {n: "updated name"} + conf3, err := snippetClient.CreateConfiguration(ctx, &pb.Configuration{ + Name: "update oc/con", + Owner: oauthService.DeviceUserID, + Resources: []*pb.Configuration_Resource{ + { + Href: configuration.ResourceURI, + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "n": "updated name", + }), + }, + TimeToLive: int64(500 * time.Millisecond), + }, + }, + }) + require.NoError(t, err) + + // skipped condition for conf1 - missing ApiAccessToken -> will be skipped during evaluation + _, err = snippetClient.CreateCondition(ctx, &pb.Condition{ + Name: "skipped update light state", + Owner: oauthService.DeviceUserID, + Enabled: true, + ConfigurationId: conf1.GetId(), + DeviceIdFilter: []string{deviceID}, + ResourceHrefFilter: []string{hubTest.TestResourceLightInstanceHref("1")}, + }) + require.NoError(t, err) + + // valid condition for conf1 + cond1, err := snippetClient.CreateCondition(ctx, &pb.Condition{ + Name: "update light state", + Owner: oauthService.DeviceUserID, + Enabled: true, + ConfigurationId: conf1.GetId(), + DeviceIdFilter: []string{deviceID}, + ResourceHrefFilter: []string{notExistingResourceHref, hubTest.TestResourceLightInstanceHref("1")}, + ApiAccessToken: token, + }) + require.NoError(t, err) + + // invalid condition for conf1 - invalid ApiAccessToken + _, err = snippetClient.CreateCondition(ctx, &pb.Condition{ + Name: "fail update light state", + Owner: oauthService.DeviceUserID, + Enabled: true, + ConfigurationId: conf1.GetId(), + DeviceIdFilter: []string{deviceID}, + ResourceHrefFilter: []string{notExistingResourceHref, hubTest.TestResourceLightInstanceHref("1")}, + ApiAccessToken: "an invalid token", + }) + require.NoError(t, err) + + // condition for conf2 + cond2, err := snippetClient.CreateCondition(ctx, &pb.Condition{ + Name: "update light power", + Owner: oauthService.DeviceUserID, + Enabled: true, + ConfigurationId: conf2.GetId(), + DeviceIdFilter: []string{deviceID}, + ResourceHrefFilter: []string{hubTest.TestResourceLightInstanceHref("1")}, + ApiAccessToken: token, + }) + require.NoError(t, err) + + // disabled condition for conf3 + _, err = snippetClient.CreateCondition(ctx, &pb.Condition{ + Name: "disabled update device name", + Owner: oauthService.DeviceUserID, + Enabled: false, + ConfigurationId: conf3.GetId(), + DeviceIdFilter: []string{deviceID}, + ResourceHrefFilter: []string{configuration.ResourceURI}, + ApiAccessToken: token, + }) + require.NoError(t, err) + // jq evaluated to false -> non matching name + _, err = snippetClient.CreateCondition(ctx, &pb.Condition{ + Name: "jq evaluated to false", + Owner: oauthService.DeviceUserID, + Enabled: true, + ConfigurationId: conf3.GetId(), + DeviceIdFilter: []string{deviceID}, + ResourceHrefFilter: []string{configuration.ResourceURI}, + ApiAccessToken: token, + JqExpressionFilter: ".n !== \"" + hubTest.TestDeviceName + "\"", + }) + require.NoError(t, err) + // invalid condition for conf3 - invalid ApiAccessToken + // -> this condition will be tried, but will fail, because of the invalid token, + // but since no other condition is available, the resource update will be set to failed state + _, err = snippetClient.CreateCondition(ctx, &pb.Condition{ + Name: "fail update device name", + Owner: oauthService.DeviceUserID, + Enabled: true, + ConfigurationId: conf3.GetId(), + DeviceIdFilter: []string{deviceID}, + ResourceHrefFilter: []string{configuration.ResourceURI}, + ApiAccessToken: "an invalid token", + JqExpressionFilter: ".n == \"" + hubTest.TestDeviceName + "\"", + }) + require.NoError(t, err) + + grpcClient := grpcgwTest.NewTestClient(t) + defer func() { + err = grpcClient.Close() + require.NoError(t, err) + }() + _, shutdownDevSim := hubTest.OnboardDevSim(ctx, t, grpcClient.GrpcGatewayClient(), deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, hubTest.GetAllBackendResourceLinks()) + defer shutdownDevSim() + + // -> wait for /conf1 to be applied -> for /not/existing resource this should start-up the timeout timer + notExistingConf1ID := conf1.GetId() + "." + notExistingResourceHref + var appliedConf1Status pb.AppliedConfiguration_Resource_Status + test.WaitForAppliedConfigurations(ctx, t, snippetClient, &pb.GetAppliedConfigurationsRequest{ + DeviceIdFilter: []string{deviceID}, + ConfigurationIdFilter: []*pb.IDFilter{ + { + Id: conf1.GetId(), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + }, + }, map[string][]pb.AppliedConfiguration_Resource_Status{ + hubTest.TestResourceLightInstanceHref("1"): {pb.AppliedConfiguration_Resource_DONE}, + notExistingResourceHref: {pb.AppliedConfiguration_Resource_TIMEOUT}, + }) + require.NotEqual(t, pb.AppliedConfiguration_Resource_QUEUED, appliedConf1Status) + + if appliedConf1Status != pb.AppliedConfiguration_Resource_TIMEOUT && + appliedConf1Status != pb.AppliedConfiguration_Resource_DONE { + // -> wait enough time to timeout pending commands + time.Sleep(2 * interval) + } + + var got map[interface{}]interface{} + err = grpcClient.GetResource(ctx, deviceID, hubTest.TestResourceLightInstanceHref("1"), &got) + require.NoError(t, err) + + require.Equal(t, map[interface{}]interface{}{ + "state": true, + "power": uint64(42), + "name": "Light", + }, got) + + // check applied configurations + appliedConfs, appliedConfResources := test.GetAppliedConfigurations(ctx, t, snippetClient, + &pb.GetAppliedConfigurationsRequest{ + DeviceIdFilter: []string{deviceID}, + ConfigurationIdFilter: []*pb.IDFilter{ + { + Id: conf1.GetId(), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + { + Id: conf2.GetId(), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + }, + }) + require.Len(t, appliedConfs, 3) + require.Len(t, appliedConfResources, 4) + + appliedConfByConfID := make(map[string]*pb.AppliedConfiguration) + for _, appliedConf := range appliedConfs { + appliedConfByConfID[appliedConf.GetConfigurationId().GetId()] = appliedConf + } + require.Equal(t, cond1.GetId(), appliedConfByConfID[conf1.GetId()].GetConditionId().GetId()) + require.Equal(t, cond2.GetId(), appliedConfByConfID[conf2.GetId()].GetConditionId().GetId()) + + notExistingConf1, ok := appliedConfResources[notExistingConf1ID] + require.True(t, ok) + require.Equal(t, notExistingResourceHref, notExistingConf1.GetHref()) + require.Equal(t, pb.AppliedConfiguration_Resource_TIMEOUT, notExistingConf1.GetStatus()) + require.Equal(t, commands.Status_ERROR, notExistingConf1.GetResourceUpdated().GetStatus()) + + lightConf1ID := conf1.GetId() + "." + hubTest.TestResourceLightInstanceHref("1") + lightConf1, ok := appliedConfResources[lightConf1ID] + require.True(t, ok) + require.Equal(t, hubTest.TestResourceLightInstanceHref("1"), lightConf1.GetHref()) + require.Equal(t, pb.AppliedConfiguration_Resource_DONE, lightConf1.GetStatus()) + require.Equal(t, commands.Status_OK, lightConf1.GetResourceUpdated().GetStatus()) + lightConf2ID := conf2.GetId() + "." + hubTest.TestResourceLightInstanceHref("1") + lightConf2, ok := appliedConfResources[lightConf2ID] + require.True(t, ok) + require.Equal(t, hubTest.TestResourceLightInstanceHref("1"), lightConf2.GetHref()) + require.Equal(t, pb.AppliedConfiguration_Resource_DONE, lightConf2.GetStatus()) + require.Equal(t, commands.Status_OK, lightConf2.GetResourceUpdated().GetStatus()) + + conConf3ID := conf3.GetId() + "." + configuration.ResourceURI + conConf3, ok := appliedConfResources[conConf3ID] + require.True(t, ok) + require.Equal(t, configuration.ResourceURI, conConf3.GetHref()) + require.Equal(t, pb.AppliedConfiguration_Resource_DONE, conConf3.GetStatus()) + require.Equal(t, commands.Status_ERROR, conConf3.GetResourceUpdated().GetStatus()) + + // restore state + err = grpcClient.UpdateResource(ctx, deviceID, hubTest.TestResourceLightInstanceHref("1"), map[string]interface{}{ + "state": false, + "power": uint64(0), + }, nil) + require.NoError(t, err) +} + +func getBridgeDeviceResources(ctx context.Context, t *testing.T, bd *bridge.Device, numResources int) (map[string]map[string]interface{}, func()) { + sdkClient, err := sdk.NewClient(bd.GetSDKClientOptions()...) + require.NoError(t, err) + defer func() { + errC := sdkClient.Close(context.Background()) + require.NoError(t, errC) + }() + + deviceID, err := sdkClient.OwnDevice(ctx, bd.GetID(), deviceClient.WithOTM(deviceClient.OTMType_JustWorks)) + require.NoError(t, err) + bd.SetID(deviceID) + + // get resource from device via SDK + bdResources := make(map[string]map[string]interface{}, numResources) + for i := range numResources { + var bdResource map[string]interface{} + err = sdkClient.GetResource(ctx, bd.GetID(), bridgeDevice.GetTestResourceHref(i), &bdResource) + require.NoError(t, err) + bdResources[bridgeDevice.GetTestResourceHref(i)] = bdResource + } + + return bdResources, func() { + for href, content := range bdResources { + err = sdkClient.UpdateResource(ctx, bd.GetID(), href, content, nil) + require.NoError(t, err) + } + } +} + +func TestServiceWithBridgedDevice(t *testing.T) { + bdConfig, err := hubTest.GetBridgeDeviceConfig() + require.NoError(t, err) + + if bdConfig.NumGeneratedBridgedDevices == 0 || bdConfig.NumResourcesPerDevice == 0 { + t.Skip("no bridge device with resources running") + } + bdName := hubTest.TestBridgeDeviceInstanceName("0") + bdID := hubTest.MustFindDeviceByName(bdName, func(d *core.Device) deviceCoap.OptionFunc { + return deviceCoap.WithQuery("di=" + d.DeviceID()) + }) + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + tearDown := hubTestService.SetUp(ctx, t) + defer tearDown() + + snippetCfg := test.MakeConfig(t) + ss, shutdownSnippetService := test.New(t, snippetCfg) + defer shutdownSnippetService() + + snippetClientConn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = snippetClientConn.Close() + }() + snippetClient := pb.NewSnippetServiceClient(snippetClientConn) + + token := oauthTest.GetDefaultAccessToken(t) + ctx = pkgGrpc.CtxWithToken(ctx, token) + + // configuration + // -> /test/%i -> { name: "new name" } + notExistingResourceHref := "/not/existing" + conf, err := snippetClient.CreateConfiguration(ctx, &pb.Configuration{ + Name: "update name", + Owner: oauthService.DeviceUserID, + Resources: func() []*pb.Configuration_Resource { + var resources []*pb.Configuration_Resource + for i := 0; i < bdConfig.NumResourcesPerDevice; i++ { + resources = append(resources, &pb.Configuration_Resource{ + Href: bridgeDevice.GetTestResourceHref(i), + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "name": "new name", + }), + }, + }) + } + resources = append(resources, &pb.Configuration_Resource{ + Href: notExistingResourceHref, + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "value": 42, + }), + }, + }) + return resources + }(), + }) + require.NoError(t, err) + require.NotEmpty(t, conf.GetId()) + + // condition for configuration + _, err = snippetClient.CreateCondition(ctx, &pb.Condition{ + Owner: oauthService.DeviceUserID, + Enabled: true, + ConfigurationId: conf.GetId(), + DeviceIdFilter: []string{bdID}, + ResourceTypeFilter: []string{bridgeDevice.TestResourceType}, + ApiAccessToken: token, + }) + require.NoError(t, err) + + grpcClient := grpcgwTest.NewTestClient(t) + defer func() { + err = grpcClient.Close() + require.NoError(t, err) + }() + + bd := bridge.NewDevice(bdID, bdName, bdConfig.NumResourcesPerDevice, true) + originalResources, restoreOriginalResources := getBridgeDeviceResources(ctx, t, bd, bdConfig.NumResourcesPerDevice) + defer restoreOriginalResources() + require.NotEmpty(t, originalResources) + + shutdownBd := hubTest.OnboardDevice(ctx, t, grpcClient.GrpcGatewayClient(), bd, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, bd.GetDefaultResources()) + defer shutdownBd() + + appliedConfResources := test.WaitForAppliedConfigurations(ctx, t, snippetClient, &pb.GetAppliedConfigurationsRequest{ + ConfigurationIdFilter: []*pb.IDFilter{ + { + Id: conf.GetId(), + Version: &pb.IDFilter_All{All: true}, + }, + }, + }, func() map[string][]pb.AppliedConfiguration_Resource_Status { + statusFilter := make(map[string][]pb.AppliedConfiguration_Resource_Status) + for i := range bdConfig.NumResourcesPerDevice { + statusFilter[bridgeDevice.GetTestResourceHref(i)] = []pb.AppliedConfiguration_Resource_Status{pb.AppliedConfiguration_Resource_DONE} + } + return statusFilter + }()) + require.Len(t, appliedConfResources, bdConfig.NumResourcesPerDevice+1) + + // force invoke configuration + _, err = snippetClient.InvokeConfiguration(ctx, &pb.InvokeConfigurationRequest{ + ConfigurationId: conf.GetId(), + DeviceId: bdID, + Force: true, + }) + require.NoError(t, err) + appliedConfResources = test.WaitForAppliedConfigurations(ctx, t, snippetClient, &pb.GetAppliedConfigurationsRequest{ + ConfigurationIdFilter: []*pb.IDFilter{ + { + Id: conf.GetId(), + Version: &pb.IDFilter_All{All: true}, + }, + }, + }, func() map[string][]pb.AppliedConfiguration_Resource_Status { + statusFilter := make(map[string][]pb.AppliedConfiguration_Resource_Status) + for i := range bdConfig.NumResourcesPerDevice { + statusFilter[bridgeDevice.GetTestResourceHref(i)] = []pb.AppliedConfiguration_Resource_Status{pb.AppliedConfiguration_Resource_DONE} + } + return statusFilter + }()) + require.Len(t, appliedConfResources, bdConfig.NumResourcesPerDevice+1) + + // cancel pending update of not existing resource + err = ss.CancelPendingResourceUpdates(ctx) + require.NoError(t, err) +} diff --git a/snippet-service/store/appliedConfiguration.go b/snippet-service/store/appliedConfiguration.go new file mode 100644 index 000000000..9664e97e0 --- /dev/null +++ b/snippet-service/store/appliedConfiguration.go @@ -0,0 +1,75 @@ +package store + +import ( + "errors" + "fmt" + + "github.com/google/uuid" + pkgMongo "github.com/plgd-dev/hub/v2/pkg/mongodb" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func ValidateAppliedConfiguration(c *pb.AppliedConfiguration) error { + if err := c.Validate(); err != nil { + return errInvalidArgument(err) + } + return nil +} + +type AppliedConfiguration struct { + RecordID string `bson:"_id"` + pb.AppliedConfiguration +} + +func MakeAppliedConfiguration(c *pb.AppliedConfiguration) AppliedConfiguration { + return AppliedConfiguration{ + AppliedConfiguration: pb.AppliedConfiguration{ + Id: c.GetId(), + DeviceId: c.GetDeviceId(), + ConfigurationId: c.GetConfigurationId().Clone(), + ExecutedBy: c.CloneExecutedBy(), + Resources: c.CloneAppliedConfiguration_Resources(), + Owner: c.GetOwner(), + Timestamp: c.GetTimestamp(), + }, + } +} + +func (c *AppliedConfiguration) GetAppliedConfiguration() *pb.AppliedConfiguration { + if c == nil { + return nil + } + return &c.AppliedConfiguration +} + +func (c *AppliedConfiguration) UnmarshalBSON(data []byte) error { + update := func(json map[string]interface{}) { + recordID, ok := json[pb.RecordIDKey] + if ok { + c.RecordID = recordID.(primitive.ObjectID).Hex() + } + delete(json, pb.RecordIDKey) + } + return pkgMongo.UnmarshalProtoBSON(data, &c.AppliedConfiguration, update) +} + +type UpdateAppliedConfigurationResourceRequest struct { + AppliedConfigurationID string + AppliedCondition *pb.AppliedConfiguration_LinkedTo + StatusFilter []pb.AppliedConfiguration_Resource_Status + Resource *pb.AppliedConfiguration_Resource +} + +func (u *UpdateAppliedConfigurationResourceRequest) Validate() error { + if _, err := uuid.Parse(u.AppliedConfigurationID); err != nil { + return errInvalidArgument(fmt.Errorf("invalid ID(%v): %w", u.AppliedConfigurationID, err)) + } + if u.Resource == nil { + return errInvalidArgument(errors.New("resource is required")) + } + if err := u.Resource.Validate(); err != nil { + return errInvalidArgument(err) + } + return nil +} diff --git a/snippet-service/store/condition.go b/snippet-service/store/condition.go new file mode 100644 index 000000000..e6fef6575 --- /dev/null +++ b/snippet-service/store/condition.go @@ -0,0 +1,144 @@ +package store + +import ( + "errors" + "slices" + + "github.com/plgd-dev/hub/v2/pkg/strings" + "github.com/plgd-dev/hub/v2/snippet-service/pb" +) + +func ValidateAndNormalizeCondition(c *pb.Condition, isUpdate bool) (*pb.Condition, error) { + if err := c.Validate(isUpdate); err != nil { + return nil, errInvalidArgument(err) + } + c2 := c.Clone() + c2.Normalize() + return c2, nil +} + +type ConditionVersion struct { + Name string `bson:"name,omitempty"` + Version uint64 `bson:"version"` + Enabled bool `bson:"enabled"` + Timestamp int64 `bson:"timestamp"` + DeviceIdFilter []string `bson:"deviceIdFilter,omitempty"` + ResourceTypeFilter []string `bson:"resourceTypeFilter,omitempty"` + ResourceHrefFilter []string `bson:"resourceHrefFilter,omitempty"` + JqExpressionFilter string `bson:"jqExpressionFilter,omitempty"` + ApiAccessToken string `bson:"apiAccessToken,omitempty"` +} + +func (cv *ConditionVersion) Copy() ConditionVersion { + return ConditionVersion{ + Name: cv.Name, + Version: cv.Version, + Enabled: cv.Enabled, + Timestamp: cv.Timestamp, + DeviceIdFilter: slices.Clone(cv.DeviceIdFilter), + ResourceTypeFilter: slices.Clone(cv.ResourceTypeFilter), + ResourceHrefFilter: slices.Clone(cv.ResourceHrefFilter), + JqExpressionFilter: cv.JqExpressionFilter, + ApiAccessToken: cv.ApiAccessToken, + } +} + +func MakeConditionVersion(c *pb.Condition) ConditionVersion { + return ConditionVersion{ + Name: c.GetName(), + Version: c.GetVersion(), + Enabled: c.GetEnabled(), + Timestamp: c.GetTimestamp(), + DeviceIdFilter: c.GetDeviceIdFilter(), + ResourceTypeFilter: c.GetResourceTypeFilter(), + ResourceHrefFilter: c.GetResourceHrefFilter(), + JqExpressionFilter: c.GetJqExpressionFilter(), + ApiAccessToken: c.GetApiAccessToken(), + } +} + +type Condition struct { + Id string `bson:"_id"` + Owner string `bson:"owner"` + ConfigurationId string `bson:"configurationId"` + Latest *ConditionVersion `bson:"latest,omitempty"` + Versions []ConditionVersion `bson:"versions,omitempty"` +} + +func MakeFirstCondition(c *pb.Condition) Condition { + version := MakeConditionVersion(c) + return Condition{ + Id: c.GetId(), + Owner: c.GetOwner(), + ConfigurationId: c.GetConfigurationId(), + Latest: &version, + Versions: []ConditionVersion{version}, + } +} + +func (c *Condition) GetLatest() (*pb.Condition, error) { + if c.Latest == nil { + return nil, errors.New("latest condition not set") + } + return &pb.Condition{ + Id: c.Id, + Owner: c.Owner, + ConfigurationId: c.ConfigurationId, + Name: c.Latest.Name, + Enabled: c.Latest.Enabled, + Version: c.Latest.Version, + Timestamp: c.Latest.Timestamp, + DeviceIdFilter: c.Latest.DeviceIdFilter, + ResourceTypeFilter: c.Latest.ResourceTypeFilter, + ResourceHrefFilter: c.Latest.ResourceHrefFilter, + JqExpressionFilter: c.Latest.JqExpressionFilter, + ApiAccessToken: c.Latest.ApiAccessToken, + }, nil +} + +func (c *Condition) RangeVersions(f func(int, *pb.Condition) bool) { + for i := range c.Versions { + cond := &pb.Condition{ + Id: c.Id, + Owner: c.Owner, + ConfigurationId: c.ConfigurationId, + Name: c.Versions[i].Name, + Enabled: c.Versions[i].Enabled, + Version: c.Versions[i].Version, + Timestamp: c.Versions[i].Timestamp, + DeviceIdFilter: c.Versions[i].DeviceIdFilter, + ResourceTypeFilter: c.Versions[i].ResourceTypeFilter, + ResourceHrefFilter: c.Versions[i].ResourceHrefFilter, + JqExpressionFilter: c.Versions[i].JqExpressionFilter, + ApiAccessToken: c.Versions[i].ApiAccessToken, + } + if !f(i, cond) { + break + } + } +} + +func (c *Condition) Clone() *Condition { + c2 := &Condition{ + Id: c.Id, + Owner: c.Owner, + ConfigurationId: c.ConfigurationId, + } + if c.Latest != nil { + latest := c.Latest.Copy() + c2.Latest = &latest + } + + for _, v := range c.Versions { + c2.Versions = append(c2.Versions, v.Copy()) + } + return c2 +} + +func ValidateAndNormalizeConditionsQuery(q *GetLatestConditionsQuery) error { + if q.DeviceID == "" && q.ResourceHref == "" && len(q.ResourceTypeFilter) == 0 { + return errInvalidArgument(errors.New("at least one condition filter must be set")) + } + q.ResourceTypeFilter = strings.Unique(q.ResourceTypeFilter) + return nil +} diff --git a/snippet-service/store/config/config.go b/snippet-service/store/config/config.go new file mode 100644 index 000000000..20220af87 --- /dev/null +++ b/snippet-service/store/config/config.go @@ -0,0 +1,9 @@ +package config + +import ( + "github.com/plgd-dev/hub/v2/pkg/config/database" + "github.com/plgd-dev/hub/v2/snippet-service/store/cqldb" + "github.com/plgd-dev/hub/v2/snippet-service/store/mongodb" +) + +type Config = database.Config[*mongodb.Config, *cqldb.Config] diff --git a/snippet-service/store/configuration.go b/snippet-service/store/configuration.go new file mode 100644 index 000000000..d38ed1239 --- /dev/null +++ b/snippet-service/store/configuration.go @@ -0,0 +1,116 @@ +package store + +import ( + "errors" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" +) + +func ValidateAndNormalizeConfiguration(c *pb.Configuration, isUpdate bool) (*pb.Configuration, error) { + if err := c.Validate(isUpdate); err != nil { + return nil, errInvalidArgument(err) + } + c2 := c.Clone() + c2.Normalize() + return c2, nil +} + +type ConfigurationVersion struct { + Name string `bson:"name,omitempty"` + Version uint64 `bson:"version"` + Resources []*pb.Configuration_Resource `bson:"resources"` + Timestamp int64 `bson:"timestamp"` +} + +func (cv *ConfigurationVersion) Copy() ConfigurationVersion { + c := ConfigurationVersion{ + Name: cv.Name, + Version: cv.Version, + Timestamp: cv.Timestamp, + } + for _, r := range cv.Resources { + c.Resources = append(c.Resources, r.Clone()) + } + return c +} + +func MakeConfigurationVersion(c *pb.Configuration) ConfigurationVersion { + return ConfigurationVersion{ + Name: c.GetName(), + Version: c.GetVersion(), + Resources: c.GetResources(), + Timestamp: c.GetTimestamp(), + } +} + +type Configuration struct { + Id string `bson:"_id"` + Owner string `bson:"owner"` + Latest *ConfigurationVersion `bson:"latest,omitempty"` + Versions []ConfigurationVersion `bson:"versions,omitempty"` +} + +func MakeFirstConfiguration(c *pb.Configuration) Configuration { + version := MakeConfigurationVersion(c) + return Configuration{ + Id: c.GetId(), + Owner: c.GetOwner(), + Latest: &version, + Versions: []ConfigurationVersion{version}, + } +} + +func (c *Configuration) GetLatest() (*pb.Configuration, error) { + if c.Latest == nil { + return nil, errors.New("latest configuration not set") + } + return &pb.Configuration{ + Id: c.Id, + Owner: c.Owner, + Version: c.Latest.Version, + Name: c.Latest.Name, + Resources: c.Latest.Resources, + Timestamp: c.Latest.Timestamp, + }, nil +} + +func (c *Configuration) RangeVersions(f func(int, *pb.Configuration) bool) { + for i := range c.Versions { + conf := &pb.Configuration{ + Id: c.Id, + Owner: c.Owner, + Name: c.Versions[i].Name, + Timestamp: c.Versions[i].Timestamp, + Version: c.Versions[i].Version, + Resources: c.Versions[i].Resources, + } + if !f(i, conf) { + break + } + } +} + +func (c *Configuration) Clone() *Configuration { + c2 := &Configuration{ + Id: c.Id, + Owner: c.Owner, + } + if c.Latest != nil { + latest := c.Latest.Copy() + c2.Latest = &latest + } + + for _, v := range c.Versions { + c2.Versions = append(c2.Versions, v.Copy()) + } + return c2 +} + +type InvokeConfigurationRequest = pb.InvokeConfigurationRequest + +func ValidateInvokeConfigurationRequest(req *InvokeConfigurationRequest) error { + if err := req.Validate(); err != nil { + return errInvalidArgument(err) + } + return nil +} diff --git a/snippet-service/store/cqldb/appliedConfiguration.go b/snippet-service/store/cqldb/appliedConfiguration.go new file mode 100644 index 000000000..6db209886 --- /dev/null +++ b/snippet-service/store/cqldb/appliedConfiguration.go @@ -0,0 +1,32 @@ +package cqldb + +import ( + "context" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" +) + +func (s *Store) GetAppliedConfigurations(context.Context, string, *pb.GetAppliedConfigurationsRequest, store.ProccessAppliedConfigurations) error { + return store.ErrNotSupported +} + +func (s *Store) DeleteAppliedConfigurations(context.Context, string, *pb.DeleteAppliedConfigurationsRequest) (int64, error) { + return 0, store.ErrNotSupported +} + +func (s *Store) CreateAppliedConfiguration(context.Context, *pb.AppliedConfiguration, bool) (*pb.AppliedConfiguration, *pb.AppliedConfiguration, error) { + return nil, nil, store.ErrNotSupported +} + +func (s *Store) InsertAppliedConfigurations(context.Context, ...*store.AppliedConfiguration) error { + return store.ErrNotSupported +} + +func (s *Store) UpdateAppliedConfigurationResource(context.Context, string, store.UpdateAppliedConfigurationResourceRequest) (*pb.AppliedConfiguration, error) { + return nil, store.ErrNotSupported +} + +func (s *Store) GetPendingAppliedConfigurationResourceUpdates(context.Context, bool, store.ProccessAppliedConfigurations) (int64, error) { + return 0, store.ErrNotSupported +} diff --git a/snippet-service/store/cqldb/condition.go b/snippet-service/store/cqldb/condition.go new file mode 100644 index 000000000..b9c3a609a --- /dev/null +++ b/snippet-service/store/cqldb/condition.go @@ -0,0 +1,32 @@ +package cqldb + +import ( + "context" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" +) + +func (s *Store) CreateCondition(context.Context, *pb.Condition) (*pb.Condition, error) { + return nil, store.ErrNotSupported +} + +func (s *Store) UpdateCondition(context.Context, *pb.Condition) (*pb.Condition, error) { + return nil, store.ErrNotSupported +} + +func (s *Store) GetConditions(context.Context, string, *pb.GetConditionsRequest, store.ProcessConditions) error { + return store.ErrNotSupported +} + +func (s *Store) DeleteConditions(context.Context, string, *pb.DeleteConditionsRequest) (int64, error) { + return 0, store.ErrNotSupported +} + +func (s *Store) InsertConditions(context.Context, ...*store.Condition) error { + return store.ErrNotSupported +} + +func (s *Store) GetLatestEnabledConditions(context.Context, string, *store.GetLatestConditionsQuery, store.ProcessConditions) error { + return store.ErrNotSupported +} diff --git a/snippet-service/store/cqldb/config.go b/snippet-service/store/cqldb/config.go new file mode 100644 index 000000000..9ef1fcdd7 --- /dev/null +++ b/snippet-service/store/cqldb/config.go @@ -0,0 +1,15 @@ +package cqldb + +import ( + "github.com/plgd-dev/hub/v2/pkg/cqldb" +) + +// Config provides Mongo DB configuration options +type Config struct { + Embedded cqldb.Config `yaml:",inline" json:",inline"` + Table string `yaml:"table" json:"table"` +} + +func (c *Config) Validate() error { + return c.Embedded.Validate() +} diff --git a/snippet-service/store/cqldb/configuration.go b/snippet-service/store/cqldb/configuration.go new file mode 100644 index 000000000..9be169354 --- /dev/null +++ b/snippet-service/store/cqldb/configuration.go @@ -0,0 +1,32 @@ +package cqldb + +import ( + "context" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" +) + +func (s *Store) CreateConfiguration(context.Context, *pb.Configuration) (*pb.Configuration, error) { + return nil, store.ErrNotSupported +} + +func (s *Store) UpdateConfiguration(context.Context, *pb.Configuration) (*pb.Configuration, error) { + return nil, store.ErrNotSupported +} + +func (s *Store) GetConfigurations(context.Context, string, *pb.GetConfigurationsRequest, store.ProcessConfigurations) error { + return store.ErrNotSupported +} + +func (s *Store) DeleteConfigurations(context.Context, string, *pb.DeleteConfigurationsRequest) (int64, error) { + return 0, store.ErrNotSupported +} + +func (s *Store) InsertConfigurations(context.Context, ...*store.Configuration) error { + return store.ErrNotSupported +} + +func (s *Store) GetLatestConfigurationsByID(context.Context, string, []string, store.ProcessConfigurations) error { + return store.ErrNotSupported +} diff --git a/snippet-service/store/cqldb/store.go b/snippet-service/store/cqldb/store.go new file mode 100644 index 000000000..2b608bf94 --- /dev/null +++ b/snippet-service/store/cqldb/store.go @@ -0,0 +1,9 @@ +package cqldb + +import ( + "github.com/plgd-dev/hub/v2/pkg/cqldb" +) + +type Store struct { + *cqldb.Store +} diff --git a/snippet-service/store/mongodb/appliedConfiguration.go b/snippet-service/store/mongodb/appliedConfiguration.go new file mode 100644 index 000000000..abe8ae4d2 --- /dev/null +++ b/snippet-service/store/mongodb/appliedConfiguration.go @@ -0,0 +1,309 @@ +package mongodb + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/pkg/mongodb" + "github.com/plgd-dev/hub/v2/pkg/strings" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func (s *Store) InsertAppliedConfigurations(ctx context.Context, confs ...*store.AppliedConfiguration) error { + documents := make([]interface{}, 0, len(confs)) + for _, conf := range confs { + documents = append(documents, conf) + } + _, err := s.Collection(appliedConfigurationsCol).InsertMany(ctx, documents) + return err +} + +func (s *Store) replaceAppliedConfiguration(ctx context.Context, newAdc *store.AppliedConfiguration) (*store.AppliedConfiguration, error) { + var replacedAdc *store.AppliedConfiguration + filter := bson.M{ + pb.OwnerKey: newAdc.GetOwner(), + pb.DeviceIDKey: newAdc.GetDeviceId(), + pb.ConfigurationLinkIDKey: newAdc.GetConfigurationId().GetId(), + } + opts := options.FindOneAndReplace().SetReturnDocument(options.Before).SetUpsert(true) + result := s.Collection(appliedConfigurationsCol).FindOneAndReplace(ctx, filter, newAdc, opts) + if result.Err() == nil { + replacedAdc = &store.AppliedConfiguration{} + if err := result.Decode(replacedAdc); err != nil { + return nil, err + } + } + if result.Err() != nil && !errors.Is(result.Err(), mongo.ErrNoDocuments) { + return nil, result.Err() + } + return replacedAdc, nil +} + +func (s *Store) CreateAppliedConfiguration(ctx context.Context, adc *pb.AppliedConfiguration, force bool) (*pb.AppliedConfiguration, *pb.AppliedConfiguration, error) { + if err := store.ValidateAppliedConfiguration(adc); err != nil { + return nil, nil, err + } + newAdc := store.MakeAppliedConfiguration(adc) + if newAdc.GetId() == "" { + newAdc.Id = uuid.NewString() + } + newAdc.Timestamp = time.Now().UnixNano() + if force { + replacedAdc, err := s.replaceAppliedConfiguration(ctx, &newAdc) + if err != nil { + return nil, nil, err + } + return newAdc.GetAppliedConfiguration(), replacedAdc.GetAppliedConfiguration(), nil + } + if _, err := s.Collection(appliedConfigurationsCol).InsertOne(ctx, &newAdc); err != nil { + return nil, nil, err + } + return newAdc.GetAppliedConfiguration(), nil, nil +} + +func toAppliedDeviceConfigurationsVersionFilter(idKey, versionsKey string, vf pb.VersionFilter) interface{} { + filters := make([]interface{}, 0, 2) + if len(vf.All) > 0 { + // all ids + if vf.All[0] == "" { + return bson.M{idKey: bson.M{mongodb.Exists: true}} + } + cidFilter := inArrayQuery(idKey, vf.All) + if cidFilter != nil { + filters = append(filters, cidFilter) + } + } + versionFilters := make([]interface{}, 0, len(vf.Versions)) + for id, versions := range vf.Versions { + version := bson.M{ + versionsKey: bson.M{mongodb.In: versions}, + } + if id != "" { + version[idKey] = id + } + // id must match and version must be in the list of versions + versionFilters = append(versionFilters, version) + } + if len(versionFilters) > 0 { + filters = append(filters, toFilter(mongodb.Or, versionFilters)) + } + return toFilter(mongodb.Or, filters) +} + +func toAppliedDeviceConfigurationsIdFilter(idFilter, deviceIdFilter []string, configurationIdFilter, conditionIdFilter pb.VersionFilter) interface{} { + filters := make([]interface{}, 0, 4) + idf := inArrayQuery(pb.IDKey, strings.Unique(idFilter)) + if idf != nil { + filters = append(filters, idf) + } + dif := inArrayQuery(pb.DeviceIDKey, strings.Unique(deviceIdFilter)) + if dif != nil { + filters = append(filters, dif) + } + confif := toAppliedDeviceConfigurationsVersionFilter(pb.ConfigurationLinkIDKey, pb.ConfigurationLinkVersionKey, configurationIdFilter) + if confif != nil { + filters = append(filters, confif) + } + condif := toAppliedDeviceConfigurationsVersionFilter(pb.ConditionLinkIDKey, pb.ConditionLinkVersionKey, conditionIdFilter) + if condif != nil { + filters = append(filters, condif) + } + return toFilter(mongodb.Or, filters) +} + +func toAppliedDeviceConfigurationsQuery(owner string, idFilter, deviceIdFilter []string, configurationIdFilter, conditionIdFilter pb.VersionFilter) interface{} { + filters := make([]interface{}, 0, 2) + if owner != "" { + filters = append(filters, bson.D{{Key: pb.OwnerKey, Value: owner}}) + } + idfilters := toAppliedDeviceConfigurationsIdFilter(idFilter, deviceIdFilter, configurationIdFilter, conditionIdFilter) + if idfilters != nil { + filters = append(filters, idfilters) + } + return toFilterQuery(mongodb.And, filters) +} + +func (s *Store) GetAppliedConfigurations(ctx context.Context, owner string, query *pb.GetAppliedConfigurationsRequest, p store.ProccessAppliedConfigurations) error { + configurationIdFilter := pb.PartitionIDFilter(query.GetConfigurationIdFilter()) + conditionIdFilter := pb.PartitionIDFilter(query.GetConditionIdFilter()) + filter := toAppliedDeviceConfigurationsQuery(owner, query.GetIdFilter(), query.GetDeviceIdFilter(), configurationIdFilter, conditionIdFilter) + cur, err := s.Collection(appliedConfigurationsCol).Find(ctx, filter) + if err != nil { + return err + } + return processCursor(ctx, cur, p) +} + +func (s *Store) DeleteAppliedConfigurations(ctx context.Context, owner string, query *pb.DeleteAppliedConfigurationsRequest) error { + _, err := s.Collection(appliedConfigurationsCol).DeleteMany(ctx, toAppliedDeviceConfigurationsQuery(owner, query.GetIdFilter(), nil, pb.VersionFilter{}, pb.VersionFilter{})) + return err +} + +func (s *Store) UpdateAppliedConfigurationResource(ctx context.Context, owner string, query store.UpdateAppliedConfigurationResourceRequest) (*pb.AppliedConfiguration, error) { + if err := query.Validate(); err != nil { + return nil, err + } + filter := bson.M{ + pb.IDKey: query.AppliedConfigurationID, + pb.ResourcesKey + "." + pb.HrefKey: query.Resource.GetHref(), + } + if owner != "" { + filter[pb.OwnerKey] = owner + } + statusFilter := bson.A{} + if len(query.StatusFilter) > 0 { + for _, status := range query.StatusFilter { + statusFilter = append(statusFilter, status.String()) + } + } + + matchResourceCond := func(alias string) bson.M { + cond := bson.M{"$eq": bson.A{"$$" + alias + "." + pb.HrefKey, query.Resource.GetHref()}} + if len(statusFilter) > 0 { + cond = bson.M{ + mongodb.And: bson.A{ + cond, + bson.M{mongodb.In: bson.A{"$$" + alias + "." + pb.StatusKey, statusFilter}}, + }, + } + } + return cond + } + + const matchFoundKey = "__matchFound" + updatedTimestamp := time.Now().UnixNano() + update := mongo.Pipeline{ + // check if we have a resource with the given href and status + bson.D{{Key: mongodb.Set, Value: bson.M{ + matchFoundKey: bson.M{"$gt": bson.A{ + bson.M{ + "$size": bson.M{ + "$filter": bson.M{ + "input": "$" + pb.ResourcesKey, + "as": "elem", + "cond": matchResourceCond("elem"), + }, + }, + }, 0, + }}, + }}}, + } + // replace the resource with the new one + update = append(update, bson.D{{Key: mongodb.Set, Value: bson.M{ + pb.ResourcesKey: bson.M{ + "$map": bson.M{ + "input": "$" + pb.ResourcesKey, + "as": "elem", + "in": bson.M{ + "$cond": bson.M{ + "if": matchResourceCond("elem"), + "then": query.Resource, + "else": "$$elem", + }, + }, + }, + }, + }}}) + + // update the timestamp and the condition relation if we have a matched resource + set := bson.M{ + pb.TimestampKey: bson.M{ + "$cond": bson.M{ + "if": "$" + matchFoundKey, + "then": updatedTimestamp, + "else": "$" + pb.TimestampKey, + }, + }, + } + if query.AppliedCondition != nil { + set[pb.ConditionLinkIDKey] = bson.M{ + "$cond": bson.M{ + "if": "$" + matchFoundKey, + "then": query.AppliedCondition.GetId(), + "else": "$" + pb.ConditionLinkIDKey, + }, + } + set[pb.ConditionLinkVersionKey] = bson.M{ + "$cond": bson.M{ + "if": "$" + matchFoundKey, + "then": query.AppliedCondition.GetVersion(), + "else": "$" + pb.ConditionLinkVersionKey, + }, + } + } + + // update the timestamp and the condition relation if we have a matched resource + update = append(update, bson.D{{Key: mongodb.Set, Value: set}}) + + // remove the __matchFound field + update = append(update, bson.D{{Key: mongodb.Unset, Value: bson.A{matchFoundKey}}}) + + opts := options.FindOneAndUpdate().SetReturnDocument(options.After) + result := s.Collection(appliedConfigurationsCol).FindOneAndUpdate(ctx, filter, update, opts) + if result.Err() != nil { + if errors.Is(result.Err(), mongo.ErrNoDocuments) { + return nil, fmt.Errorf("%w: %w", store.ErrNotFound, fmt.Errorf("no applied configuration(%v) with resource(%v)", query.AppliedConfigurationID, query.Resource.GetHref())) + } + return nil, result.Err() + } + + updatedAppliedCfg := store.AppliedConfiguration{} + err := result.Decode(&updatedAppliedCfg) + if err != nil { + return nil, err + } + // check timestamp to know whether the resource was updated or not + if updatedAppliedCfg.GetTimestamp() != updatedTimestamp { + return nil, fmt.Errorf("%w: %w", store.ErrNotModified, fmt.Errorf("applied configuration(%v) was not updated", query.AppliedConfigurationID)) + } + + return updatedAppliedCfg.GetAppliedConfiguration(), nil +} + +func (s *Store) GetPendingAppliedConfigurationResourceUpdates(ctx context.Context, expiredOnly bool, p store.ProccessAppliedConfigurations) (int64, error) { + var validUntil int64 + // match resources that have a resource in pending state + match := bson.M{ + pb.StatusKey: pb.AppliedConfiguration_Resource_PENDING.String(), + } + // project only the resources that are pending + andCond := bson.A{ + bson.M{"$eq": bson.A{"$$this." + pb.StatusKey, pb.AppliedConfiguration_Resource_PENDING.String()}}, + } + if expiredOnly { + validUntil = time.Now().UnixNano() + match[pb.ValidUntil] = bson.M{"$lte": validUntil} + andCond = append(andCond, bson.M{"$gt": bson.A{"$$this." + pb.ValidUntil, 0}}) + andCond = append(andCond, bson.M{"$lte": bson.A{"$$this." + pb.ValidUntil, validUntil}}) + } + pl := mongo.Pipeline{ + bson.D{{Key: mongodb.Match, Value: bson.M{ + pb.ResourcesKey: bson.M{ + "$elemMatch": match, + }, + }}}, + } + pl = append(pl, bson.D{{Key: mongodb.Set, Value: bson.M{ + pb.ResourcesKey: bson.M{ + "$filter": bson.M{ + "input": "$" + pb.ResourcesKey, + "cond": bson.M{ + mongodb.And: andCond, + }, + }, + }, + }}}) + + cur, err := s.Collection(appliedConfigurationsCol).Aggregate(ctx, pl) + if err != nil { + return 0, err + } + return validUntil, processCursor(ctx, cur, p) +} diff --git a/snippet-service/store/mongodb/condition.go b/snippet-service/store/mongodb/condition.go new file mode 100644 index 000000000..35f704e69 --- /dev/null +++ b/snippet-service/store/mongodb/condition.go @@ -0,0 +1,234 @@ +package mongodb + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/hashicorp/go-multierror" + "github.com/plgd-dev/hub/v2/pkg/mongodb" + "github.com/plgd-dev/hub/v2/pkg/strings" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func (s *Store) InsertConditions(ctx context.Context, conds ...*store.Condition) error { + documents := make([]interface{}, 0, len(conds)) + for _, cond := range conds { + documents = append(documents, cond) + } + _, err := s.Collection(conditionsCol).InsertMany(ctx, documents) + return err +} + +func (s *Store) CreateCondition(ctx context.Context, cond *pb.Condition) (*pb.Condition, error) { + newCond, err := store.ValidateAndNormalizeCondition(cond, false) + if err != nil { + return nil, err + } + if newCond.GetId() == "" { + newCond.Id = uuid.NewString() + } + newCond.Timestamp = time.Now().UnixNano() + storeCond := store.MakeFirstCondition(newCond) + _, err = s.Collection(conditionsCol).InsertOne(ctx, storeCond) + if err != nil { + return nil, err + } + return storeCond.GetLatest() +} + +func filterCondition(cond *pb.Condition) bson.M { + filter := bson.M{ + pb.RecordIDKey: cond.GetId(), + pb.OwnerKey: cond.GetOwner(), + } + if cond.GetConfigurationId() != "" { + filter[pb.ConfigurationIDKey] = cond.GetConfigurationId() + } + if cond.GetVersion() != 0 { + // if is set -> it must be higher than the $latest.version + filter[pb.LatestKey+"."+pb.VersionKey] = bson.M{"$lt": cond.GetVersion()} + } + return filter +} + +func updateCondition(cond *pb.Condition) mongo.Pipeline { + pl := mongo.Pipeline{ + bson.D{{Key: mongodb.Set, Value: bson.M{ + temporaryLatestKey: store.MakeConditionVersion(cond), + }}}, + } + // if the version is not forced then look at the version of the last latest configuration + // and increment it by 1 + if cond.GetVersion() == 0 { + pl = append(pl, + bson.D{{Key: mongodb.Set, Value: incrementLatestVersion(temporaryLatestKey + "." + pb.VersionKey)}}) + } + pl = append(pl, + bson.D{{Key: mongodb.Set, Value: bson.M{ + pb.LatestKey: "$" + temporaryLatestKey, + }}}, + bson.D{{Key: mongodb.Unset, Value: bson.A{temporaryLatestKey}}}, + bson.D{{Key: mongodb.Set, Value: bson.M{ + pb.VersionsKey: appendLatestToVersions(), + }}}) + return pl +} + +func (s *Store) UpdateCondition(ctx context.Context, cond *pb.Condition) (*pb.Condition, error) { + newCond, err := store.ValidateAndNormalizeCondition(cond, true) + if err != nil { + return nil, err + } + + filter := filterCondition(newCond) + newCond.Timestamp = time.Now().UnixNano() + update := updateCondition(newCond) + opts := options.FindOneAndUpdate().SetReturnDocument(options.After).SetProjection(bson.M{pb.VersionsKey: false}) + result := s.Collection(conditionsCol).FindOneAndUpdate(ctx, filter, update, opts) + if result.Err() != nil { + return nil, result.Err() + } + + updatedCond := &store.Condition{} + err = result.Decode(&updatedCond) + if err != nil { + return nil, err + } + return updatedCond.GetLatest() +} + +// getConditionsByID returns full condition documents matched by ID +func (s *Store) getConditionsByID(ctx context.Context, owner string, ids []string, p store.ProcessConditions) error { + cur, err := s.Collection(conditionsCol).Find(ctx, toIdFilterQuery(owner, toIdQuery(ids), false)) + if err != nil { + return err + } + return processCursor(ctx, cur, p) +} + +func toIdOrConfIdQuery(ids, confIds []string) bson.M { + filter := make([]bson.M, 0, 2) + idFilter := inArrayQuery(pb.RecordIDKey, ids) + if len(idFilter) > 0 { + filter = append(filter, idFilter) + } + confIdFilter := inArrayQuery(pb.ConfigurationIDKey, confIds) + if len(confIdFilter) > 0 { + filter = append(filter, confIdFilter) + } + if len(filter) == 0 { + return nil + } + if len(filter) == 1 { + return filter[0] + } + return bson.M{mongodb.Or: filter} +} + +// getLatestConditions returns the latest condition from document matched by condition ID or configuration ID +func (s *Store) getLatestConditions(ctx context.Context, owner string, ids, confIds []string, p store.ProcessConditions) error { + opt := options.Find().SetProjection(bson.M{pb.VersionsKey: false}) + cur, err := s.Collection(conditionsCol).Find(ctx, toIdFilterQuery(owner, toIdOrConfIdQuery(ids, confIds), false), opt) + if err != nil { + return err + } + return processCursor(ctx, cur, p) +} + +// getConditionsByAggregation returns conditions matched by ID and versions +func (s *Store) getConditionsByAggregation(ctx context.Context, owner, id string, versions []uint64, p store.ProcessConditions) error { + cur, err := s.Collection(conditionsCol).Aggregate(ctx, getPipeline(owner, id, versions)) + if err != nil { + return err + } + return processCursor(ctx, cur, p) +} + +func (s *Store) GetConditions(ctx context.Context, owner string, query *pb.GetConditionsRequest, p store.Process[store.Condition]) error { + vf := pb.PartitionIDFilter(query.GetIdFilter()) + confIdLatestFilter := strings.Unique(query.GetConfigurationIdFilter()) + var errors *multierror.Error + if len(vf.All) > 0 || vf.IsEmpty() && len(confIdLatestFilter) == 0 { + err := s.getConditionsByID(ctx, owner, vf.All, p) + errors = multierror.Append(errors, err) + } + + if len(vf.Latest) > 0 || len(confIdLatestFilter) > 0 { + err := s.getLatestConditions(ctx, owner, vf.Latest, query.GetConfigurationIdFilter(), p) + errors = multierror.Append(errors, err) + } + + for id, vf := range vf.Versions { + err := s.getConditionsByAggregation(ctx, owner, id, vf, p) + errors = multierror.Append(errors, err) + } + return errors.ErrorOrNil() +} + +func (s *Store) DeleteConditions(ctx context.Context, owner string, query *pb.DeleteConditionsRequest) error { + return s.delete(ctx, conditionsCol, owner, query.GetIdFilter()) +} + +func toLatestEnabledQueryFilter() bson.D { + key := pb.LatestKey + "." + pb.EnabledKey + return bson.D{{Key: key, Value: true}} +} + +func toLatestDeviceIDQueryFilter(deviceID string) bson.M { + key := pb.LatestKey + "." + pb.DeviceIDFilterKey + return bson.M{mongodb.Or: bson.A{ + bson.M{key: bson.M{mongodb.Exists: false}}, + bson.M{key: deviceID}, + }} +} + +func toLatestResourceHrefQueryFilter(resourceHref string) bson.M { + key := pb.LatestKey + "." + pb.ResourceHrefFilterKey + return bson.M{mongodb.Or: bson.A{ + bson.M{key: bson.M{mongodb.Exists: false}}, + bson.M{key: resourceHref}, + }} +} + +func toLatestResouceTypeQueryFilter(resourceTypeFilter []string) bson.M { + key := pb.LatestKey + "." + pb.ResourceTypeFilterKey + return bson.M{mongodb.Or: bson.A{ + bson.M{key: bson.M{mongodb.Exists: false}}, + bson.M{key: bson.M{mongodb.All: resourceTypeFilter}}, + }} +} + +func toLatestConditionsQuery(owner string, queries *store.GetLatestConditionsQuery) interface{} { + filters := make([]interface{}, 0, 5) + filters = append(filters, toLatestEnabledQueryFilter()) + if owner != "" { + filters = append(filters, bson.D{{Key: pb.OwnerKey, Value: owner}}) + } + if queries.DeviceID != "" { + filters = append(filters, toLatestDeviceIDQueryFilter(queries.DeviceID)) + } + if queries.ResourceHref != "" { + filters = append(filters, toLatestResourceHrefQueryFilter(queries.ResourceHref)) + } + if len(queries.ResourceTypeFilter) > 0 { + filters = append(filters, toLatestResouceTypeQueryFilter(queries.ResourceTypeFilter)) + } + return toFilterQuery(mongodb.And, filters) +} + +func (s *Store) GetLatestEnabledConditions(ctx context.Context, owner string, query *store.GetLatestConditionsQuery, p store.ProcessConditions) error { + if err := store.ValidateAndNormalizeConditionsQuery(query); err != nil { + return err + } + opt := options.Find().SetProjection(bson.M{pb.VersionsKey: false}) + cur, err := s.Collection(conditionsCol).Find(ctx, toLatestConditionsQuery(owner, query), opt) + if err != nil { + return err + } + return processCursor(ctx, cur, p) +} diff --git a/snippet-service/store/mongodb/config.go b/snippet-service/store/mongodb/config.go new file mode 100644 index 000000000..8034a68c7 --- /dev/null +++ b/snippet-service/store/mongodb/config.go @@ -0,0 +1,13 @@ +package mongodb + +import ( + pkgMongo "github.com/plgd-dev/hub/v2/pkg/mongodb" +) + +type Config struct { + Mongo pkgMongo.Config `yaml:",inline"` +} + +func (c *Config) Validate() error { + return c.Mongo.Validate() +} diff --git a/snippet-service/store/mongodb/configuration.go b/snippet-service/store/mongodb/configuration.go new file mode 100644 index 000000000..d9f89b1a4 --- /dev/null +++ b/snippet-service/store/mongodb/configuration.go @@ -0,0 +1,151 @@ +package mongodb + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/hashicorp/go-multierror" + "github.com/plgd-dev/hub/v2/pkg/mongodb" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func (s *Store) InsertConfigurations(ctx context.Context, confs ...*store.Configuration) error { + documents := make([]interface{}, 0, len(confs)) + for _, conf := range confs { + documents = append(documents, conf) + } + _, err := s.Collection(configurationsCol).InsertMany(ctx, documents) + return err +} + +func (s *Store) CreateConfiguration(ctx context.Context, conf *pb.Configuration) (*pb.Configuration, error) { + newConf, err := store.ValidateAndNormalizeConfiguration(conf, false) + if err != nil { + return nil, err + } + if newConf.GetId() == "" { + newConf.Id = uuid.NewString() + } + newConf.Timestamp = time.Now().UnixNano() + storeConf := store.MakeFirstConfiguration(newConf) + _, err = s.Collection(configurationsCol).InsertOne(ctx, storeConf) + if err != nil { + return nil, err + } + return storeConf.GetLatest() +} + +func filterConfiguration(conf *pb.Configuration) bson.M { + filter := bson.M{ + pb.RecordIDKey: conf.GetId(), + pb.OwnerKey: conf.GetOwner(), + } + if conf.GetVersion() != 0 { + // if is set -> it must be higher than the $latest.version + filter[pb.LatestKey+"."+pb.VersionKey] = bson.M{"$lt": conf.GetVersion()} + } + return filter +} + +func updateConfiguration(conf *pb.Configuration) mongo.Pipeline { + pl := mongo.Pipeline{ + bson.D{{Key: mongodb.Set, Value: bson.M{ + temporaryLatestKey: store.MakeConfigurationVersion(conf), + }}}, + } + // if the version is not forced then look at the version of the last latest configuration + // and increment it by 1 + if conf.GetVersion() == 0 { + pl = append(pl, + bson.D{{Key: mongodb.Set, Value: incrementLatestVersion(temporaryLatestKey + "." + pb.VersionKey)}}) + } + pl = append(pl, + bson.D{{Key: mongodb.Set, Value: bson.M{ + pb.LatestKey: "$" + temporaryLatestKey, + }}}, + bson.D{{Key: mongodb.Unset, Value: bson.A{temporaryLatestKey}}}, + bson.D{{Key: mongodb.Set, Value: bson.M{ + pb.VersionsKey: appendLatestToVersions(), + }}}) + return pl +} + +func (s *Store) UpdateConfiguration(ctx context.Context, conf *pb.Configuration) (*pb.Configuration, error) { + newConf, err := store.ValidateAndNormalizeConfiguration(conf, true) + if err != nil { + return nil, err + } + + filter := filterConfiguration(newConf) + newConf.Timestamp = time.Now().UnixNano() + update := updateConfiguration(newConf) + opts := options.FindOneAndUpdate().SetReturnDocument(options.After).SetProjection(bson.M{pb.VersionsKey: false}) + result := s.Collection(configurationsCol).FindOneAndUpdate(ctx, filter, update, opts) + if result.Err() != nil { + return nil, result.Err() + } + + updatedCfg := store.Configuration{} + err = result.Decode(&updatedCfg) + if err != nil { + return nil, err + } + return updatedCfg.GetLatest() +} + +// getConfigurationsByID returns all configurations from documents matched by ID +func (s *Store) getConfigurationsByID(ctx context.Context, owner string, ids []string, p store.ProcessConfigurations) error { + cur, err := s.Collection(configurationsCol).Find(ctx, toIdFilterQuery(owner, toIdQuery(ids), false)) + if err != nil { + return err + } + return processCursor(ctx, cur, p) +} + +// GetLatestConfigurationsByID returns the latest configuration from documents matched by ID +func (s *Store) GetLatestConfigurationsByID(ctx context.Context, owner string, ids []string, p store.ProcessConfigurations) error { + opt := options.Find().SetProjection(bson.M{pb.VersionsKey: false}) + cur, err := s.Collection(configurationsCol).Find(ctx, toIdFilterQuery(owner, toIdQuery(ids), false), opt) + if err != nil { + return err + } + return processCursor(ctx, cur, p) +} + +// getConfigurationsByAggregation returns conditions matched by ID and versions +func (s *Store) getConfigurationsByAggregation(ctx context.Context, owner, id string, versions []uint64, p store.ProcessConfigurations) error { + cur, err := s.Collection(configurationsCol).Aggregate(ctx, getPipeline(owner, id, versions)) + if err != nil { + return err + } + return processCursor(ctx, cur, p) +} + +func (s *Store) GetConfigurations(ctx context.Context, owner string, query *pb.GetConfigurationsRequest, p store.Process[store.Configuration]) error { + vf := pb.PartitionIDFilter(query.GetIdFilter()) + var errors *multierror.Error + if len(vf.All) > 0 || vf.IsEmpty() { + err := s.getConfigurationsByID(ctx, owner, vf.All, p) + errors = multierror.Append(errors, err) + } + + if len(vf.Latest) > 0 { + err := s.GetLatestConfigurationsByID(ctx, owner, vf.Latest, p) + errors = multierror.Append(errors, err) + } + + for id, versions := range vf.Versions { + err := s.getConfigurationsByAggregation(ctx, owner, id, versions, p) + errors = multierror.Append(errors, err) + } + return errors.ErrorOrNil() +} + +func (s *Store) DeleteConfigurations(ctx context.Context, owner string, query *pb.DeleteConfigurationsRequest) error { + return s.delete(ctx, configurationsCol, owner, query.GetIdFilter()) +} diff --git a/snippet-service/store/mongodb/createAppliedConfiguration_test.go b/snippet-service/store/mongodb/createAppliedConfiguration_test.go new file mode 100644 index 000000000..9d45820c4 --- /dev/null +++ b/snippet-service/store/mongodb/createAppliedConfiguration_test.go @@ -0,0 +1,324 @@ +package mongodb_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/resource-aggregate/events" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/test" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" + "github.com/stretchr/testify/require" +) + +func TestStoreCreateAppliedConfiguration(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + appliedConfID1 := uuid.NewString() + owner := "owner" + deviceID := "deviceID1" + confID1 := uuid.NewString() + condID1 := uuid.NewString() + executedBy1 := &pb.AppliedConfiguration_ConditionId{ + ConditionId: &pb.AppliedConfiguration_LinkedTo{ + Id: condID1, + }, + } + resource1 := &pb.AppliedConfiguration_Resource{ + Href: "/test/1", + CorrelationId: "correlationID1", + Status: pb.AppliedConfiguration_Resource_DONE, + ResourceUpdated: &events.ResourceUpdated{ + ResourceId: commands.NewResourceID(deviceID, hubTest.TestResourceLightInstanceHref("1")), + Content: &commands.Content{ + CoapContentFormat: -1, + }, + Status: commands.Status_OK, + AuditContext: commands.NewAuditContext(oauthService.DeviceUserID, "", oauthService.DeviceUserID), + ResourceTypes: hubTest.TestResourceLightInstanceResourceTypes, + }, + } + + appliedConf1, _, err := s.CreateAppliedConfiguration(ctx, &pb.AppliedConfiguration{ + Id: appliedConfID1, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID1}, + ExecutedBy: executedBy1, + Resources: []*pb.AppliedConfiguration_Resource{resource1}, + }, false) + require.NoError(t, err) + + appliedConfID2 := uuid.NewString() + confID2 := uuid.NewString() + executedBy2 := &pb.AppliedConfiguration_OnDemand{ + OnDemand: true, + } + resource2 := &pb.AppliedConfiguration_Resource{ + Href: "/test/2", + CorrelationId: "correlationID2", + Status: pb.AppliedConfiguration_Resource_TIMEOUT, + } + appliedConfID3 := uuid.NewString() + appliedConfID4 := uuid.NewString() + confID3 := uuid.NewString() + + type args struct { + adc *pb.AppliedConfiguration + force bool + } + tests := []struct { + name string + args args + wantErr bool + want *pb.AppliedConfiguration + wantReplaced *pb.AppliedConfiguration + }{ + { + name: "invalid ID", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: "invalid", + }, + }, + wantErr: true, + }, + { + name: "missing owner", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID2, + }, + }, + wantErr: true, + }, + { + name: "missing deviceID", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID2, + Owner: owner, + }, + }, + wantErr: true, + }, + { + name: "missing configuration", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID2, + Owner: owner, + DeviceId: deviceID, + }, + }, + wantErr: true, + }, + { + name: "invalid configurationID", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID2, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{}, + }, + }, + wantErr: true, + }, + { + name: "missing executedBy", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID2, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID2}, + }, + }, + wantErr: true, + }, + { + name: "missing resources", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID2, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID2}, + ExecutedBy: executedBy2, + }, + }, + wantErr: true, + }, + { + name: "invalid resource - missing href", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID2, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID2}, + ExecutedBy: executedBy2, + Resources: []*pb.AppliedConfiguration_Resource{ + {}, + }, + }, + }, + wantErr: true, + }, + { + name: "invalid resource - missing correlationID", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID2, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID2}, + ExecutedBy: executedBy2, + Resources: []*pb.AppliedConfiguration_Resource{ + { + Href: "/href", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "duplicate ID", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID1, + Owner: owner, + DeviceId: "deviceID2", + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: uuid.NewString()}, + ExecutedBy: executedBy2, + Resources: []*pb.AppliedConfiguration_Resource{resource1}, + }, + }, + wantErr: true, + }, + { + name: "duplicate deviceID + configurationID", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: uuid.NewString(), + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID1}, + ExecutedBy: executedBy1, + Resources: []*pb.AppliedConfiguration_Resource{resource1}, + }, + }, + wantErr: true, + }, + { + name: "new", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID2, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID2}, + ExecutedBy: executedBy2, + Resources: []*pb.AppliedConfiguration_Resource{resource2}, + }, + }, + want: &pb.AppliedConfiguration{ + Id: appliedConfID2, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID2}, + ExecutedBy: executedBy2, + Resources: []*pb.AppliedConfiguration_Resource{resource2}, + }, + }, + { + name: "force duplicate deviceID + configurationID", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID3, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID1}, + ExecutedBy: executedBy2, + Resources: []*pb.AppliedConfiguration_Resource{resource2}, + }, + force: true, + }, + want: &pb.AppliedConfiguration{ + Id: appliedConfID3, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID1}, + ExecutedBy: executedBy2, + Resources: []*pb.AppliedConfiguration_Resource{resource2}, + }, + wantReplaced: appliedConf1, + }, + { + name: "new (force)", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: appliedConfID4, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID3}, + ExecutedBy: executedBy2, + Resources: []*pb.AppliedConfiguration_Resource{resource2}, + }, + force: true, + }, + want: &pb.AppliedConfiguration{ + Id: appliedConfID4, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID3}, + ExecutedBy: executedBy2, + Resources: []*pb.AppliedConfiguration_Resource{resource2}, + }, + }, + { + // force allows to create a new applied configuration with the same deviceID and configurationID + // however, the owner must match + name: "fail force duplicate deviceID + configurationID - mismatched owner", + args: args{ + adc: &pb.AppliedConfiguration{ + Id: uuid.NewString(), + Owner: "mismatched", + DeviceId: deviceID, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{Id: confID1}, + ExecutedBy: executedBy2, + Resources: []*pb.AppliedConfiguration_Resource{resource2}, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotReplaced, err := s.CreateAppliedConfiguration(ctx, tt.args.adc, tt.args.force) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + if tt.wantReplaced != nil { + require.NotNil(t, gotReplaced) + test.CmpAppliedDeviceConfiguration(t, tt.wantReplaced, gotReplaced, true) + } else { + require.Nil(t, gotReplaced) + } + test.CmpAppliedDeviceConfiguration(t, tt.want, got, true) + }) + } +} diff --git a/snippet-service/store/mongodb/createCondition_test.go b/snippet-service/store/mongodb/createCondition_test.go new file mode 100644 index 000000000..57cac6326 --- /dev/null +++ b/snippet-service/store/mongodb/createCondition_test.go @@ -0,0 +1,134 @@ +package mongodb_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestStoreCreateCondition(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + condID := uuid.NewString() + confID := uuid.NewString() + + type args struct { + cond *pb.Condition + } + tests := []struct { + name string + args args + wantErr bool + want func(*testing.T, *pb.Condition) + }{ + { + name: "invalid ID", + args: args{ + cond: &pb.Condition{ + Id: "invalid", + ConfigurationId: uuid.NewString(), + Owner: "owner", + }, + }, + wantErr: true, + }, + { + name: "missing ConfigurationId", + args: args{ + cond: &pb.Condition{ + Id: uuid.NewString(), + Owner: "owner", + }, + }, + wantErr: true, + }, + { + name: "invalid ConfigurationId", + args: args{ + cond: &pb.Condition{ + Id: uuid.NewString(), + ConfigurationId: "invalid", + Owner: "owner", + }, + }, + wantErr: true, + }, + { + name: "missing owner", + args: args{ + cond: &pb.Condition{ + Id: uuid.NewString(), + ConfigurationId: uuid.NewString(), + }, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + cond: &pb.Condition{ + Id: condID, + ConfigurationId: confID, + Owner: "owner", + }, + }, + want: func(t *testing.T, got *pb.Condition) { + want := &pb.Condition{ + Id: condID, + ConfigurationId: confID, + Owner: "owner", + } + test.CmpCondition(t, want, got, true) + }, + }, + { + name: "valid (generated ID)", + args: args{ + cond: &pb.Condition{ + ConfigurationId: uuid.NewString(), + Owner: "owner", + }, + }, + want: func(t *testing.T, got *pb.Condition) { + want := &pb.Condition{ + Id: got.GetId(), + ConfigurationId: got.GetConfigurationId(), + Owner: "owner", + } + test.CmpCondition(t, want, got, true) + }, + }, + { + name: "duplicit ID", + args: args{ + cond: &pb.Condition{ + Id: condID, + ConfigurationId: uuid.NewString(), + Owner: "owner", + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := s.CreateCondition(ctx, tt.args.cond) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tt.want(t, got) + }) + } +} diff --git a/snippet-service/store/mongodb/createConfiguration_test.go b/snippet-service/store/mongodb/createConfiguration_test.go new file mode 100644 index 000000000..18f73ea98 --- /dev/null +++ b/snippet-service/store/mongodb/createConfiguration_test.go @@ -0,0 +1,157 @@ +package mongodb_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/test" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func makeLightResourceConfiguration(t *testing.T, id string, power int, ttl int64) *pb.Configuration_Resource { + return &pb.Configuration_Resource{ + Href: hubTest.TestResourceLightInstanceHref(id), + Content: &commands.Content{ + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "power": power, + }), + ContentType: message.AppOcfCbor.String(), + CoapContentFormat: int32(message.AppOcfCbor), + }, + TimeToLive: ttl, + } +} + +func TestStoreCreateConfiguration(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + confID := uuid.NewString() + const owner = "owner1" + resources := []*pb.Configuration_Resource{ + makeLightResourceConfiguration(t, "1", 1, 1337), + } + + type args struct { + create *pb.Configuration + } + tests := []struct { + name string + args args + wantErr bool + want func(*testing.T, *pb.Configuration) + }{ + { + name: "invalid ID", + args: args{ + create: &pb.Configuration{ + Id: "invalid", + Name: "invalid ID", + Owner: owner, + Version: 0, + Resources: resources, + }, + }, + wantErr: true, + }, + { + name: "missing owner", + args: args{ + create: &pb.Configuration{ + Id: confID, + Name: "missing owner", + Version: 0, + Resources: resources, + }, + }, + wantErr: true, + }, + { + name: "missing resources", + args: args{ + create: &pb.Configuration{ + Id: confID, + Name: "missing resources", + Owner: owner, + Version: 0, + }, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + create: &pb.Configuration{ + Id: confID, + Name: "valid", + Owner: owner, + Resources: resources, + }, + }, + want: func(t *testing.T, got *pb.Configuration) { + wantCfg := &pb.Configuration{ + Id: confID, + Name: "valid", + Owner: owner, + Resources: resources, + } + test.CmpConfiguration(t, wantCfg, got, true) + }, + }, + { + name: "valid - generated ID", + args: args{ + create: &pb.Configuration{ + Name: "valid", + Owner: owner, + Version: 37, + Resources: resources, + }, + }, + want: func(t *testing.T, got *pb.Configuration) { + wantCfg := &pb.Configuration{ + Id: got.GetId(), + Name: "valid", + Owner: owner, + Version: 37, + Resources: resources, + } + test.CmpConfiguration(t, wantCfg, got, true) + }, + }, + { + name: "duplicit item (ID)", + args: args{ + create: &pb.Configuration{ + Id: confID, + Name: "duplicit ID", + Owner: owner, + Resources: resources, + Version: 42, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := s.CreateConfiguration(ctx, tt.args.create) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tt.want(t, got) + }) + } +} diff --git a/snippet-service/store/mongodb/deleteAppliedConfiguration_test.go b/snippet-service/store/mongodb/deleteAppliedConfiguration_test.go new file mode 100644 index 000000000..9fcb94cb1 --- /dev/null +++ b/snippet-service/store/mongodb/deleteAppliedConfiguration_test.go @@ -0,0 +1,151 @@ +package mongodb_test + +import ( + "context" + "testing" + "time" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/plgd-dev/hub/v2/snippet-service/store/mongodb" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/stretchr/testify/require" +) + +func TestStoreDeleteAppliedConfigurations(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + getAppliedConfigurations := func(t *testing.T, s *mongodb.Store, owner string, query *pb.GetAppliedConfigurationsRequest) []*pb.AppliedConfiguration { + var configurations []*pb.AppliedConfiguration + err := s.GetAppliedConfigurations(ctx, owner, query, func(c *store.AppliedConfiguration) error { + configurations = append(configurations, c.GetAppliedConfiguration().Clone()) + return nil + }) + require.NoError(t, err) + return configurations + } + + getAppliedConfigurationsMap := func(t *testing.T, s *mongodb.Store, owner string, query *pb.GetAppliedConfigurationsRequest) map[string]*pb.AppliedConfiguration { + confs := getAppliedConfigurations(t, s, owner, query) + confsMap := make(map[string]*pb.AppliedConfiguration) + for _, conf := range confs { + confsMap[conf.GetId()] = conf + } + return confsMap + } + + type args struct { + owner string + query *pb.DeleteAppliedConfigurationsRequest + } + tests := []struct { + name string + args args + wantErr bool + want func(t *testing.T, s *mongodb.Store, stored map[string]*pb.AppliedConfiguration) + }{ + { + name: "all", + args: args{}, + want: func(t *testing.T, s *mongodb.Store, _ map[string]*pb.AppliedConfiguration) { + confs := getAppliedConfigurations(t, s, "", nil) + require.Empty(t, confs) + }, + }, + { + name: "owner2", + args: args{ + owner: test.Owner(1), + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]*pb.AppliedConfiguration) { + confsMap := getAppliedConfigurationsMap(t, s, "", nil) + require.NotEmpty(t, confsMap) + newStored := make(map[string]*pb.AppliedConfiguration) + for _, conf := range stored { + if conf.GetOwner() == test.Owner(1) { + continue + } + newStored[conf.GetId()] = conf + } + test.CmpAppliedDeviceConfigurationsMaps(t, newStored, confsMap, false) + }, + }, + { + name: "id{1,3,4,5}", + args: args{ + query: &pb.DeleteAppliedConfigurationsRequest{ + IdFilter: []string{ + test.AppliedConfigurationID(1), + test.AppliedConfigurationID(3), + test.AppliedConfigurationID(4), + test.AppliedConfigurationID(5), + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]*pb.AppliedConfiguration) { + confsMap := getAppliedConfigurationsMap(t, s, "", nil) + require.NotEmpty(t, confsMap) + newStored := make(map[string]*pb.AppliedConfiguration) + + for _, conf := range stored { + confID := conf.GetId() + if confID == test.AppliedConfigurationID(1) || + confID == test.AppliedConfigurationID(3) || + confID == test.AppliedConfigurationID(4) || + confID == test.AppliedConfigurationID(5) { + continue + } + newStored[confID] = conf + } + test.CmpAppliedDeviceConfigurationsMaps(t, newStored, confsMap, false) + }, + }, + { + name: "owner2/id2", + args: args{ + owner: test.Owner(2), + query: &pb.DeleteAppliedConfigurationsRequest{ + IdFilter: []string{ + test.AppliedConfigurationID(2), + // Ids not owned by owner2 should not be deleted + test.AppliedConfigurationID(1), + test.AppliedConfigurationID(3), + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]*pb.AppliedConfiguration) { + confsMap := getAppliedConfigurationsMap(t, s, "", nil) + require.NotEmpty(t, confsMap) + newStored := make(map[string]*pb.AppliedConfiguration) + for _, conf := range stored { + confID := conf.GetId() + if confID == test.AppliedConfigurationID(2) { + continue + } + newStored[confID] = conf + } + test.CmpAppliedDeviceConfigurationsMaps(t, newStored, confsMap, false) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + inserted := test.AddAppliedConfigurationsToStore(ctx, t, s) + err := s.DeleteAppliedConfigurations(ctx, tt.args.owner, tt.args.query) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + stored := make(map[string]*pb.AppliedConfiguration) + for id, conf := range inserted { + stored[id] = conf.GetAppliedConfiguration() + } + tt.want(t, s, stored) + }) + } +} diff --git a/snippet-service/store/mongodb/deleteConditions_test.go b/snippet-service/store/mongodb/deleteConditions_test.go new file mode 100644 index 000000000..ed6595dcf --- /dev/null +++ b/snippet-service/store/mongodb/deleteConditions_test.go @@ -0,0 +1,357 @@ +package mongodb_test + +import ( + "context" + "testing" + "time" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/plgd-dev/hub/v2/snippet-service/store/mongodb" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/stretchr/testify/require" +) + +func TestStoreDeleteConditions(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + getConditions := func(t *testing.T, s *mongodb.Store, owner string, query *pb.GetConditionsRequest) []*store.Condition { + var conditions []*store.Condition + err := s.GetConditions(ctx, owner, query, func(c *store.Condition) error { + conditions = append(conditions, c.Clone()) + return nil + }) + require.NoError(t, err) + return conditions + } + + getConditionsMap := func(t *testing.T, s *mongodb.Store, owner string, query *pb.GetConditionsRequest) map[string]store.Condition { + conds := getConditions(t, s, owner, query) + condsMap := make(map[string]store.Condition) + for _, cond := range conds { + condsMap[cond.Id] = *cond + } + return condsMap + } + + type args struct { + owner string + query *pb.DeleteConditionsRequest + makeQuery func(stored map[string]store.Condition) *pb.DeleteConditionsRequest + } + tests := []struct { + name string + args args + wantErr bool + want func(t *testing.T, s *mongodb.Store, stored map[string]store.Condition) + }{ + { + name: "all", + args: args{}, + want: func(t *testing.T, s *mongodb.Store, _ map[string]store.Condition) { + conds := getConditions(t, s, "", nil) + require.Empty(t, conds) + }, + }, + { + name: "owner1", + args: args{ + owner: test.Owner(1), + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Condition) { + condsMap := getConditionsMap(t, s, "", nil) + require.NotEmpty(t, condsMap) + newStored := make(map[string]store.Condition) + for _, cond := range stored { + if cond.Owner == test.Owner(1) { + continue + } + newStored[cond.Id] = cond + } + test.CmpStoredConditionMaps(t, newStored, condsMap) + }, + }, + { + name: "id{1,3,4,5}", + args: args{ + query: &pb.DeleteConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConditionID(1), + Version: &pb.IDFilter_All{All: true}, + }, + { + Id: test.ConditionID(3), + Version: &pb.IDFilter_All{All: true}, + }, + { + Id: test.ConditionID(4), + Version: &pb.IDFilter_All{All: true}, + }, + { + Id: test.ConditionID(5), + Version: &pb.IDFilter_All{All: true}, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Condition) { + condsMap := getConditionsMap(t, s, "", nil) + require.NotEmpty(t, condsMap) + newStored := make(map[string]store.Condition) + for _, cond := range stored { + if cond.Id == test.ConditionID(1) || + cond.Id == test.ConditionID(3) || + cond.Id == test.ConditionID(4) || + cond.Id == test.ConditionID(5) { + continue + } + newStored[cond.Id] = cond + } + test.CmpStoredConditionMaps(t, newStored, condsMap) + }, + }, + { + name: "owner2/id2", + args: args{ + owner: test.Owner(2), + query: &pb.DeleteConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConditionID(2), + Version: &pb.IDFilter_All{All: true}, + }, + // Ids not owned by owner2 should not be deleted + { + Id: test.ConditionID(1), + Version: &pb.IDFilter_All{All: true}, + }, + { + Id: test.ConditionID(3), + Version: &pb.IDFilter_All{All: true}, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Condition) { + condsMap := getConditionsMap(t, s, "", nil) + require.NotEmpty(t, condsMap) + newStored := make(map[string]store.Condition) + for _, cond := range stored { + if cond.Id == test.ConditionID(2) { + continue + } + newStored[cond.Id] = cond + } + test.CmpStoredConditionMaps(t, newStored, condsMap) + }, + }, + { + name: "latest", + args: args{ + query: &pb.DeleteConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Condition) { + for id, cond := range stored { + cond.Versions = cond.Versions[:len(cond.Versions)-1] + latest := cond.Versions[len(cond.Versions)-1].Copy() + cond.Latest = &latest + stored[id] = cond + } + confsMap := getConditionsMap(t, s, "", nil) + require.NotEmpty(t, confsMap) + test.CmpStoredConditionMaps(t, stored, confsMap) + }, + }, + { + name: "owner1/latest", + args: args{ + owner: test.Owner(1), + query: &pb.DeleteConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + // duplicates should be ignored + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Condition) { + for id, cond := range stored { + if cond.Owner != test.Owner(1) { + continue + } + cond.Versions = cond.Versions[:len(cond.Versions)-1] + latest := cond.Versions[len(cond.Versions)-1].Copy() + cond.Latest = &latest + stored[id] = cond + } + condsMap := getConditionsMap(t, s, "", nil) + require.NotEmpty(t, condsMap) + test.CmpStoredConditionMaps(t, stored, condsMap) + }, + }, + { + name: "owner2/id1/latest - non-matching owner", + args: args{ + owner: test.Owner(2), + query: &pb.DeleteConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConditionID(1), + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Condition) { + condsMap := getConditionsMap(t, s, "", nil) + test.CmpStoredConditionMaps(t, stored, condsMap) + }, + }, + { + name: "version/{13, 113, 213, 313, 413, 513}", args: args{ + owner: "", + query: &pb.DeleteConditionsRequest{ + IdFilter: []*pb.IDFilter{ + {Version: &pb.IDFilter_Value{Value: 13}}, + {Version: &pb.IDFilter_Value{Value: 113}}, + {Version: &pb.IDFilter_Value{Value: 213}}, + {Version: &pb.IDFilter_Value{Value: 313}}, + {Version: &pb.IDFilter_Value{Value: 413}}, + {Version: &pb.IDFilter_Value{Value: 513}}, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Condition) { + condsMap := getConditionsMap(t, s, "", nil) + for _, cond := range stored { + versions := make([]store.ConditionVersion, 0) + for _, version := range cond.Versions { + if version.Version == 13 || + version.Version == 113 || + version.Version == 213 || + version.Version == 313 || + version.Version == 413 || + version.Version == 513 { + continue + } + versions = append(versions, version) + } + cond.Versions = versions + stored[cond.Id] = cond + } + test.CmpStoredConditionMaps(t, stored, condsMap) + }, + }, + { + name: "owner2/version/{207, 213, 221}", args: args{ + owner: test.Owner(2), + query: &pb.DeleteConditionsRequest{ + IdFilter: []*pb.IDFilter{ + {Version: &pb.IDFilter_Value{Value: 207}}, + {Version: &pb.IDFilter_Value{Value: 213}}, + {Version: &pb.IDFilter_Value{Value: 221}}, + // duplicates should be ignored + {Version: &pb.IDFilter_Value{Value: 213}}, + // filter with Id should be ignored if there are filters without Id + { + Id: test.ConditionID(2), + Version: &pb.IDFilter_Value{Value: 213}, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Condition) { + condsMap := getConditionsMap(t, s, "", nil) + for _, cond := range stored { + if cond.Owner == test.Owner(2) { + versions := make([]store.ConditionVersion, 0) + for _, version := range cond.Versions { + if version.Version == 207 || + version.Version == 213 || + version.Version == 221 { + continue + } + versions = append(versions, version) + } + cond.Versions = versions + stored[cond.Id] = cond + } + } + test.CmpStoredConditionMaps(t, stored, condsMap) + }, + }, + { + name: "id7/version/{$(all)}", + args: args{ + makeQuery: func(stored map[string]store.Condition) *pb.DeleteConditionsRequest { + r := &pb.DeleteConditionsRequest{} + id7, ok := stored[test.ConditionID(7)] + require.True(t, ok) + for _, version := range id7.Versions { + r.IdFilter = append(r.IdFilter, &pb.IDFilter{ + Id: test.ConditionID(7), + Version: &pb.IDFilter_Value{Value: version.Version}, + }) + } + return r + }, + }, + want: func(t *testing.T, s *mongodb.Store, _ map[string]store.Condition) { + conds := getConditionsMap(t, s, "", &pb.GetConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConditionID(7), + }, + }, + }) + require.Empty(t, conds) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + inserted := test.AddConditionsToStore(ctx, t, s, 500, func(iteration int) uint64 { + return uint64(iteration * 100) + }) + query := tt.args.query + if tt.args.makeQuery != nil { + query = tt.args.makeQuery(inserted) + } + err := s.DeleteConditions(ctx, tt.args.owner, query) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tt.want(t, s, inserted) + }) + } +} diff --git a/snippet-service/store/mongodb/deleteConfigurations_test.go b/snippet-service/store/mongodb/deleteConfigurations_test.go new file mode 100644 index 000000000..a0a447745 --- /dev/null +++ b/snippet-service/store/mongodb/deleteConfigurations_test.go @@ -0,0 +1,359 @@ +package mongodb_test + +import ( + "context" + "testing" + "time" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/plgd-dev/hub/v2/snippet-service/store/mongodb" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/stretchr/testify/require" +) + +func TestStoreDeleteConfigurations(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + getConfigurations := func(t *testing.T, s *mongodb.Store, owner string, query *pb.GetConfigurationsRequest) []*store.Configuration { + var configurations []*store.Configuration + err := s.GetConfigurations(ctx, owner, query, func(c *store.Configuration) error { + configurations = append(configurations, c.Clone()) + return nil + }) + require.NoError(t, err) + return configurations + } + + getConfigurationsMap := func(t *testing.T, s *mongodb.Store, owner string, query *pb.GetConfigurationsRequest) map[string]store.Configuration { + confs := getConfigurations(t, s, owner, query) + confsMap := make(map[string]store.Configuration) + for _, conf := range confs { + confsMap[conf.Id] = *conf + } + return confsMap + } + + type args struct { + owner string + query *pb.DeleteConfigurationsRequest + makeQuery func(stored map[string]store.Configuration) *pb.DeleteConfigurationsRequest + } + tests := []struct { + name string + args args + wantErr bool + want func(t *testing.T, s *mongodb.Store, stored map[string]store.Configuration) + }{ + { + name: "all", + args: args{}, + want: func(t *testing.T, s *mongodb.Store, _ map[string]store.Configuration) { + confs := getConfigurations(t, s, "", nil) + require.Empty(t, confs) + }, + }, + { + name: "owner1", + args: args{ + owner: test.Owner(1), + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Configuration) { + confsMap := getConfigurationsMap(t, s, "", nil) + require.NotEmpty(t, confsMap) + newStored := make(map[string]store.Configuration) + for _, conf := range stored { + if conf.Owner == test.Owner(1) { + continue + } + newStored[conf.Id] = conf + } + test.CmpStoredConfigurationMaps(t, newStored, confsMap) + }, + }, + { + name: "id{1,3,4,5}", + args: args{ + query: &pb.DeleteConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConfigurationID(1), + Version: &pb.IDFilter_All{All: true}, + }, + { + Id: test.ConfigurationID(3), + Version: &pb.IDFilter_All{All: true}, + }, + { + Id: test.ConfigurationID(4), + Version: &pb.IDFilter_All{All: true}, + }, + { + Id: test.ConfigurationID(5), + Version: &pb.IDFilter_All{All: true}, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Configuration) { + confsMap := getConfigurationsMap(t, s, "", nil) + require.NotEmpty(t, confsMap) + newStored := make(map[string]store.Configuration) + for _, conf := range stored { + if conf.Id == test.ConfigurationID(1) || + conf.Id == test.ConfigurationID(3) || + conf.Id == test.ConfigurationID(4) || + conf.Id == test.ConfigurationID(5) { + continue + } + newStored[conf.Id] = conf + } + test.CmpStoredConfigurationMaps(t, newStored, confsMap) + }, + }, + { + name: "owner2/id2", + args: args{ + owner: test.Owner(2), + query: &pb.DeleteConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConfigurationID(2), + Version: &pb.IDFilter_All{All: true}, + }, + // Ids not owned by owner2 should not be deleted + { + Id: test.ConfigurationID(1), + Version: &pb.IDFilter_All{All: true}, + }, + { + Id: test.ConfigurationID(3), + Version: &pb.IDFilter_All{All: true}, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Configuration) { + confsMap := getConfigurationsMap(t, s, "", nil) + require.NotEmpty(t, confsMap) + newStored := make(map[string]store.Configuration) + for _, conf := range stored { + if conf.Id == test.ConfigurationID(2) { + continue + } + newStored[conf.Id] = conf + } + test.CmpStoredConfigurationMaps(t, newStored, confsMap) + }, + }, + { + name: "latest", + args: args{ + query: &pb.DeleteConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Configuration) { + for id, conf := range stored { + conf.Versions = conf.Versions[:len(conf.Versions)-1] + latest := conf.Versions[len(conf.Versions)-1].Copy() + conf.Latest = &latest + stored[id] = conf + } + confsMap := getConfigurationsMap(t, s, "", nil) + require.NotEmpty(t, confsMap) + test.CmpStoredConfigurationMaps(t, stored, confsMap) + }, + }, + { + name: "owner1/latest", + args: args{ + owner: test.Owner(1), + query: &pb.DeleteConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + // duplicates should be ignored + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Configuration) { + for id, conf := range stored { + if conf.Owner != test.Owner(1) { + continue + } + conf.Versions = conf.Versions[:len(conf.Versions)-1] + latest := conf.Versions[len(conf.Versions)-1].Copy() + conf.Latest = &latest + stored[id] = conf + } + confsMap := getConfigurationsMap(t, s, "", nil) + require.NotEmpty(t, confsMap) + test.CmpStoredConfigurationMaps(t, stored, confsMap) + }, + }, + { + name: "owner2/id1/latest - non-matching owner", + args: args{ + owner: test.Owner(2), + query: &pb.DeleteConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConfigurationID(1), + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Configuration) { + confsMap := getConfigurationsMap(t, s, "", nil) + test.CmpStoredConfigurationMaps(t, stored, confsMap) + }, + }, + { + name: "version/{42, 142, 242, 342, 442, 542}", + args: args{ + owner: "", + query: &pb.DeleteConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + {Version: &pb.IDFilter_Value{Value: 42}}, + {Version: &pb.IDFilter_Value{Value: 142}}, + {Version: &pb.IDFilter_Value{Value: 242}}, + {Version: &pb.IDFilter_Value{Value: 342}}, + {Version: &pb.IDFilter_Value{Value: 442}}, + {Version: &pb.IDFilter_Value{Value: 542}}, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Configuration) { + confsMap := getConfigurationsMap(t, s, "", nil) + for _, conf := range stored { + versions := make([]store.ConfigurationVersion, 0) + for _, version := range conf.Versions { + if version.Version == 42 || + version.Version == 142 || + version.Version == 242 || + version.Version == 342 || + version.Version == 442 || + version.Version == 542 { + continue + } + versions = append(versions, version) + } + conf.Versions = versions + stored[conf.Id] = conf + } + test.CmpStoredConfigurationMaps(t, stored, confsMap) + }, + }, + { + name: "owner2/version/{213, 237, 242}", + args: args{ + owner: test.Owner(2), + query: &pb.DeleteConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + {Version: &pb.IDFilter_Value{Value: 213}}, + {Version: &pb.IDFilter_Value{Value: 237}}, + {Version: &pb.IDFilter_Value{Value: 242}}, + // duplicates should be ignored + {Version: &pb.IDFilter_Value{Value: 237}}, + // filter with Id should be ignored if there are filters without Id + { + Id: test.ConfigurationID(2), + Version: &pb.IDFilter_Value{Value: 237}, + }, + }, + }, + }, + want: func(t *testing.T, s *mongodb.Store, stored map[string]store.Configuration) { + confsMap := getConfigurationsMap(t, s, "", nil) + for _, conf := range stored { + if conf.Owner == test.Owner(2) { + versions := make([]store.ConfigurationVersion, 0) + for _, version := range conf.Versions { + if version.Version == 213 || + version.Version == 237 || + version.Version == 242 { + continue + } + versions = append(versions, version) + } + conf.Versions = versions + stored[conf.Id] = conf + } + } + test.CmpStoredConfigurationMaps(t, stored, confsMap) + }, + }, + { + name: "id7/version/{$(all)}", + args: args{ + makeQuery: func(stored map[string]store.Configuration) *pb.DeleteConfigurationsRequest { + r := &pb.DeleteConfigurationsRequest{} + id7, ok := stored[test.ConfigurationID(7)] + require.True(t, ok) + for _, version := range id7.Versions { + r.IdFilter = append(r.IdFilter, &pb.IDFilter{ + Id: test.ConfigurationID(7), + Version: &pb.IDFilter_Value{Value: version.Version}, + }) + } + return r + }, + }, + want: func(t *testing.T, s *mongodb.Store, _ map[string]store.Configuration) { + confs := getConfigurations(t, s, "", &pb.GetConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConfigurationID(7), + }, + }, + }) + require.Empty(t, confs) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + inserted := test.AddConfigurationsToStore(ctx, t, s, 500, func(iteration int) uint64 { + return uint64(iteration * 100) + }) + query := tt.args.query + if tt.args.makeQuery != nil { + query = tt.args.makeQuery(inserted) + } + err := s.DeleteConfigurations(ctx, tt.args.owner, query) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tt.want(t, s, inserted) + }) + } +} diff --git a/snippet-service/store/mongodb/getAppliedConfigurations_test.go b/snippet-service/store/mongodb/getAppliedConfigurations_test.go new file mode 100644 index 000000000..4835076dc --- /dev/null +++ b/snippet-service/store/mongodb/getAppliedConfigurations_test.go @@ -0,0 +1,440 @@ +package mongodb_test + +import ( + "context" + "testing" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestStoreGetAppliedConfigurations(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + appliedConfs := test.AddAppliedConfigurationsToStore(ctx, t, s) + + type args struct { + owner string + query *pb.GetAppliedConfigurationsRequest + } + tests := []struct { + name string + args args + wantErr bool + want func(*store.AppliedConfiguration) bool + }{ + { + name: "all", + args: args{}, + want: func(*store.AppliedConfiguration) bool { + return true + }, + }, + { + name: "owner0", + args: args{ + owner: test.Owner(0), + }, + want: func(ac *store.AppliedConfiguration) bool { + return ac.GetOwner() == test.Owner(0) + }, + }, + { + name: "id/{1, 3, 5}", + args: args{ + query: &pb.GetAppliedConfigurationsRequest{ + IdFilter: []string{ + test.AppliedConfigurationID(1), + test.AppliedConfigurationID(3), + test.AppliedConfigurationID(5), + // duplicates should be ignored + test.AppliedConfigurationID(5), + test.AppliedConfigurationID(5), + }, + }, + }, + want: func(ac *store.AppliedConfiguration) bool { + acID := ac.GetId() + return acID == test.AppliedConfigurationID(1) || acID == test.AppliedConfigurationID(3) || acID == test.AppliedConfigurationID(5) + }, + }, + { + name: "owner1/id{0, 1, 2, 3, 4, 5}", + args: args{ + owner: test.Owner(1), + query: &pb.GetAppliedConfigurationsRequest{ + IdFilter: []string{ + test.AppliedConfigurationID(0), + test.AppliedConfigurationID(1), + test.AppliedConfigurationID(2), + test.AppliedConfigurationID(3), + test.AppliedConfigurationID(4), + test.AppliedConfigurationID(5), + }, + }, + }, + want: func(ac *store.AppliedConfiguration) bool { + acID := ac.GetId() + return (ac.GetOwner() == test.Owner(1)) && + (acID == test.AppliedConfigurationID(0) || acID == test.AppliedConfigurationID(1) || acID == test.AppliedConfigurationID(2) || + acID == test.AppliedConfigurationID(3) || acID == test.AppliedConfigurationID(4) || acID == test.AppliedConfigurationID(5)) + }, + }, + { + name: "deviceId/{0, 2}", + args: args{ + query: &pb.GetAppliedConfigurationsRequest{ + DeviceIdFilter: []string{ + test.DeviceID(0), + test.DeviceID(2), + // duplicates should be ignored + test.DeviceID(2), + test.DeviceID(0), + }, + }, + }, + want: func(ac *store.AppliedConfiguration) bool { + acDeviceID := ac.GetDeviceId() + return acDeviceID == test.DeviceID(0) || acDeviceID == test.DeviceID(2) + }, + }, + { + name: "owner2/{id{1, 2, 5} + deviceId{1, 3}}", + args: args{ + owner: test.Owner(2), + query: &pb.GetAppliedConfigurationsRequest{ + IdFilter: []string{ + test.AppliedConfigurationID(1), + test.AppliedConfigurationID(2), + test.AppliedConfigurationID(5), + }, + DeviceIdFilter: []string{ + test.DeviceID(1), + test.DeviceID(3), + }, + }, + }, + want: func(ac *store.AppliedConfiguration) bool { + acID := ac.GetId() + acDeviceID := ac.GetDeviceId() + return ac.GetOwner() == test.Owner(2) && + ((acID == test.AppliedConfigurationID(1) || acID == test.AppliedConfigurationID(2) || acID == test.AppliedConfigurationID(5)) || + (acDeviceID == test.DeviceID(1) || acDeviceID == test.DeviceID(3))) + }, + }, + { + // get all owner0 configurations with a linkend configuration (should be all) + name: "owner0/configurationId/all", + args: args{ + owner: test.Owner(0), + query: &pb.GetAppliedConfigurationsRequest{ + ConfigurationIdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_All{ + All: true, + }, + }, + }, + }, + }, + want: func(ac *store.AppliedConfiguration) bool { + return ac.GetOwner() == test.Owner(0) + }, + }, + { + name: "owner1/configurationId/id{1, 3, 7}", + args: args{ + owner: test.Owner(1), + query: &pb.GetAppliedConfigurationsRequest{ + ConfigurationIdFilter: []*pb.IDFilter{ + { + Id: test.ConfigurationID(1), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + { + Id: test.ConfigurationID(3), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + { + Id: test.ConfigurationID(7), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + }, + }, + }, + want: func(ac *store.AppliedConfiguration) bool { + acConfID := ac.GetConfigurationId().GetId() + return ac.GetOwner() == test.Owner(1) && + (acConfID == test.ConfigurationID(1) || acConfID == test.ConfigurationID(3) || acConfID == test.ConfigurationID(7)) + }, + }, + { + name: "configurationId/version{1, 3, 7}", + args: args{ + query: &pb.GetAppliedConfigurationsRequest{ + ConfigurationIdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Value{ + Value: 1, + }, + }, + { + Version: &pb.IDFilter_Value{ + Value: 3, + }, + }, + { + Version: &pb.IDFilter_Value{ + Value: 7, + }, + }, + }, + }, + }, + + want: func(ac *store.AppliedConfiguration) bool { + acConfVersion := ac.GetConfigurationId().GetVersion() + return acConfVersion == 1 || acConfVersion == 3 || acConfVersion == 7 + }, + }, + { + name: "owner0/{deviceId{0, 2} + configurationId/version{1, 3}}", + args: args{ + owner: test.Owner(0), + query: &pb.GetAppliedConfigurationsRequest{ + DeviceIdFilter: []string{test.DeviceID(0), test.DeviceID(2)}, + ConfigurationIdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Value{ + Value: 1, + }, + }, + { + Version: &pb.IDFilter_Value{ + Value: 3, + }, + }, + }, + }, + }, + want: func(ac *store.AppliedConfiguration) bool { + if ac.GetOwner() != test.Owner(0) { + return false + } + acConfVersion := ac.GetConfigurationId().GetVersion() + acDeviceID := ac.GetDeviceId() + return (acConfVersion == 1 || acConfVersion == 3) || + (acDeviceID == test.DeviceID(0) || acDeviceID == test.DeviceID(2)) + }, + }, + { + // get all owner0 configurations with a linked condition (should be all) + name: "owner1/conditionId/all", + args: args{ + owner: test.Owner(1), + query: &pb.GetAppliedConfigurationsRequest{ + ConditionIdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_All{ + All: true, + }, + }, + }, + }, + }, + want: func(ac *store.AppliedConfiguration) bool { + return ac.GetOwner() == test.Owner(1) + }, + }, + { + name: "owner2/conditionId/id{0, 2, 4, 6}", + args: args{ + owner: test.Owner(2), + query: &pb.GetAppliedConfigurationsRequest{ + ConditionIdFilter: []*pb.IDFilter{ + { + Id: test.ConditionID(0), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + { + Id: test.ConditionID(4), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + { + Id: test.ConditionID(8), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + { + Id: test.ConditionID(12), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + }, + }, + }, + want: func(ac *store.AppliedConfiguration) bool { + acCondID := ac.GetConditionId().GetId() + return ac.GetOwner() == test.Owner(2) && + (acCondID == test.ConditionID(0) || acCondID == test.ConditionID(4) || acCondID == test.ConditionID(8) || acCondID == test.ConditionID(12)) + }, + }, + { + name: "owner0/{deviceId{0, 2} + conditionId/version{7, 9}}", + args: args{ + owner: test.Owner(0), + query: &pb.GetAppliedConfigurationsRequest{ + DeviceIdFilter: []string{test.DeviceID(0), test.DeviceID(2)}, + ConditionIdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Value{ + Value: 7, + }, + }, + { + Version: &pb.IDFilter_Value{ + Value: 9, + }, + }, + }, + }, + }, + want: func(ac *store.AppliedConfiguration) bool { + acCondVersion := ac.GetConditionId().GetVersion() + acDeviceID := ac.GetDeviceId() + return (ac.GetOwner() == test.Owner(0)) && ((acCondVersion == 7 || acCondVersion == 9) || + (acDeviceID == test.DeviceID(0) || acDeviceID == test.DeviceID(2))) + }, + }, + { + name: "{id{0, 1} + deviceId{1, 2} + configurationId/id{3, 4} + conditionId/version{5, 6}}", + args: args{ + query: &pb.GetAppliedConfigurationsRequest{ + IdFilter: []string{test.AppliedConfigurationID(0), test.AppliedConfigurationID(1)}, + DeviceIdFilter: []string{test.DeviceID(1), test.DeviceID(2)}, + ConfigurationIdFilter: []*pb.IDFilter{ + { + Id: test.ConfigurationID(3), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + { + Id: test.ConfigurationID(4), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + }, + ConditionIdFilter: []*pb.IDFilter{ + { + Id: test.ConditionID(5), + Version: &pb.IDFilter_Value{ + Value: 5, + }, + }, + { + Id: test.ConditionID(6), + Version: &pb.IDFilter_Value{ + Value: 6, + }, + }, + }, + }, + }, + want: func(ac *store.AppliedConfiguration) bool { + acID := ac.GetId() + acDeviceID := ac.GetDeviceId() + acConfID := ac.GetConfigurationId().GetId() + acCondID := ac.GetConditionId().GetId() + acCondVersion := ac.GetConditionId().GetVersion() + return (acID == test.AppliedConfigurationID(0) || acID == test.AppliedConfigurationID(1)) || + (acDeviceID == test.DeviceID(1) || acDeviceID == test.DeviceID(2)) || + (acConfID == test.ConfigurationID(3) || acConfID == test.ConfigurationID(4)) || + ((acCondID == test.ConditionID(5) && acCondVersion == 5) || + (acCondID == test.ConditionID(6) && acCondVersion == 6)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + appliedConfigurations := make(map[string]*pb.AppliedConfiguration) + err := s.GetAppliedConfigurations(ctx, tt.args.owner, tt.args.query, func(c *store.AppliedConfiguration) error { + appliedConfigurations[c.GetId()] = c.GetAppliedConfiguration().Clone() + return nil + }) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + stored := make(map[string]*pb.AppliedConfiguration) + for _, ac := range appliedConfs { + if tt.want(ac) { + stored[ac.GetId()] = ac.GetAppliedConfiguration() + } + } + require.Len(t, appliedConfigurations, len(stored)) + for _, c := range appliedConfigurations { + ac, ok := stored[c.GetId()] + require.True(t, ok) + test.CmpJSON(t, ac, c) + } + }) + } +} + +func TestGetExpiredAppliedConfigurationResourceUpdates(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + stored := test.AddAppliedConfigurationsToStore(ctx, t, s) + + got := make(map[string]*pb.AppliedConfiguration) + validUntil, err := s.GetPendingAppliedConfigurationResourceUpdates(ctx, true, func(ac *store.AppliedConfiguration) error { + got[ac.GetId()] = ac.GetAppliedConfiguration().Clone() + return nil + }) + require.NoError(t, err) + + expiredStored := make(map[string]*pb.AppliedConfiguration) + for _, ac := range stored { + var resources []*pb.AppliedConfiguration_Resource + for _, r := range ac.GetResources() { + if r.GetStatus() == pb.AppliedConfiguration_Resource_PENDING && + r.GetValidUntil() > 0 && r.GetValidUntil() <= validUntil { + resources = append(resources, r) + } + } + if len(resources) > 0 { + newAc := ac.Clone() + newAc.Resources = resources + expiredStored[ac.GetId()] = newAc + } + } + + require.Len(t, got, len(expiredStored)) + test.CmpAppliedDeviceConfigurationsMaps(t, expiredStored, got, false) +} diff --git a/snippet-service/store/mongodb/getConditions_test.go b/snippet-service/store/mongodb/getConditions_test.go new file mode 100644 index 000000000..cf6b1ce8d --- /dev/null +++ b/snippet-service/store/mongodb/getConditions_test.go @@ -0,0 +1,413 @@ +package mongodb_test + +import ( + "cmp" + "context" + "slices" + "testing" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestStoreGetConditions(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + conds := test.AddConditionsToStore(ctx, t, s, 500, nil) + + type args struct { + owner string + query *pb.GetConditionsRequest + } + tests := []struct { + name string + args args + wantErr bool + want func(t *testing.T, conditions map[string]store.Condition) + }{ + { + name: "all", + args: args{}, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.Len(t, conditions, len(conds)) + for _, c := range conditions { + cond, ok := conds[c.Id] + require.True(t, ok) + test.CmpStoredCondition(t, &cond, &c, false, true) + } + }, + }, + { + name: "owner0", + args: args{ + owner: test.Owner(0), + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.NotEmpty(t, conditions) + for _, c := range conditions { + require.Equal(t, test.Owner(0), c.Owner) + cond, ok := conds[c.Id] + require.True(t, ok) + test.CmpStoredCondition(t, &cond, &c, false, true) + } + }, + }, + { + name: "id1/all", + args: args{ + query: &pb.GetConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConditionID(1), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.Len(t, conditions, 1) + c, ok := conditions[test.ConditionID(1)] + require.True(t, ok) + cond, ok := conds[c.Id] + require.True(t, ok) + test.CmpStoredCondition(t, &cond, &c, false, true) + }, + }, + { + name: "owner2/id2/all", + args: args{ + owner: test.Owner(2), + query: &pb.GetConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConditionID(2), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.Len(t, conditions, 1) + c, ok := conditions[test.ConditionID(2)] + require.True(t, ok) + cond, ok := conds[c.Id] + require.True(t, ok) + require.Equal(t, test.ConditionID(2), cond.Id) + require.Equal(t, test.Owner(2), cond.Owner) + test.CmpStoredCondition(t, &cond, &c, false, true) + }, + }, + { + name: "latest", + args: args{ + query: &pb.GetConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.Len(t, conditions, test.RuntimeConfig.NumConditions) + for _, c := range conditions { + cond, ok := conds[c.Id] + require.True(t, ok) + require.Equal(t, cond.Id, c.Id) + require.Equal(t, cond.Owner, c.Owner) + require.Equal(t, cond.ConfigurationId, c.ConfigurationId) + require.Empty(t, c.Versions) + test.CmpJSON(t, cond.Latest, c.Latest) + } + }, + }, + { + name: "owner1/latest", + args: args{ + owner: test.Owner(1), + query: &pb.GetConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.Len(t, conditions, 7) + for _, c := range conditions { + cond, ok := conds[c.Id] + require.True(t, ok) + require.Equal(t, test.Owner(1), cond.Owner) + require.Empty(t, c.Versions) + test.CmpJSON(t, cond.Latest, c.Latest) + } + }, + }, + { + name: "owner2/id1/latest - non-matching owner", + args: args{ + owner: test.Owner(2), + query: &pb.GetConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConditionID(1), + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.Empty(t, conditions) + }, + }, + { + name: "owner2{latest, id2/latest, id5/latest} - non-matching owner", + args: args{ + owner: test.Owner(2), + query: &pb.GetConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + { + Id: test.ConditionID(2), + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + { + Id: test.ConditionID(5), + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.Len(t, conditions, 6) + for _, c := range conditions { + cond, ok := conds[c.Id] + require.True(t, ok) + require.Equal(t, test.Owner(2), cond.Owner) + require.Empty(t, c.Versions) + test.CmpJSON(t, cond.Latest, c.Latest) + } + }, + }, + { + name: "version/42", args: args{ + query: &pb.GetConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Value{ + Value: 13, + }, + }, + }, + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.Len(t, conditions, test.RuntimeConfig.NumConditions) + for _, c := range conditions { + _, ok := conds[c.Id] + require.True(t, ok) + require.Len(t, c.Versions, 1) + require.Equal(t, uint64(13), c.Versions[0].Version) + } + }, + }, + { + name: "owner2/version/{7, 13, 19}", args: args{ + owner: test.Owner(2), + query: &pb.GetConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Value{ + Value: 7, + }, + }, + { + Version: &pb.IDFilter_Value{ + Value: 13, + }, + }, + { + Version: &pb.IDFilter_Value{ + Value: 19, + }, + }, + // duplicates should be ignored + { + Version: &pb.IDFilter_Value{ + Value: 7, + }, + }, + // filter with Id should be ignored if there are filters without Id + { + Id: test.ConditionID(2), + Version: &pb.IDFilter_Value{ + Value: 19, + }, + }, + }, + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.Len(t, conditions, 6) + for _, c := range conditions { + _, ok := conds[c.Id] + require.True(t, ok) + require.Len(t, c.Versions, 3) + slices.SortFunc(c.Versions, func(i, j store.ConditionVersion) int { + return cmp.Compare(i.Version, j.Version) + }) + require.Equal(t, uint64(7), c.Versions[0].Version) + require.Equal(t, uint64(13), c.Versions[1].Version) + require.Equal(t, uint64(19), c.Versions[2].Version) + } + }, + }, + { + name: "id0/version/{1..max} + latest", + args: args{ + query: &pb.GetConditionsRequest{ + IdFilter: func() []*pb.IDFilter { + var idFilters []*pb.IDFilter + c := conds[test.ConditionID(0)] + for _, v := range c.Versions { + idFilters = append(idFilters, &pb.IDFilter{ + Id: test.ConditionID(0), + Version: &pb.IDFilter_Value{ + Value: v.Version, + }, + }) + } + idFilters = append(idFilters, &pb.IDFilter{ + Id: test.ConditionID(0), + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }) + return idFilters + }(), + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.Len(t, conditions, 1) + for _, c := range conditions { + cond, ok := conds[c.Id] + require.True(t, ok) + require.Equal(t, test.ConditionID(0), cond.Id) + test.CmpStoredCondition(t, &cond, &c, true, false) + } + }, + }, + { + name: "confId3/latest", + args: args{ + query: &pb.GetConditionsRequest{ + ConfigurationIdFilter: []string{test.ConfigurationID(3)}, + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + require.NotEmpty(t, conditions) + for _, c := range conditions { + cond, ok := conds[c.Id] + require.True(t, ok) + require.Equal(t, test.ConfigurationID(3), cond.ConfigurationId) + require.Empty(t, c.Versions) + test.CmpJSON(t, cond.Latest, c.Latest) + } + }, + }, + { + name: "owner1/{confId1, confId3}/latest", + args: args{ + owner: test.Owner(1), + query: &pb.GetConditionsRequest{ + ConfigurationIdFilter: []string{test.ConfigurationID(1), test.ConfigurationID(3)}, + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + stored := make(map[string]store.Condition) + for _, c := range conds { + if c.Owner == test.Owner(1) && + (c.ConfigurationId == test.ConfigurationID(1) || c.ConfigurationId == test.ConfigurationID(3)) { + c.Versions = nil + stored[c.Id] = c + } + } + test.CmpStoredConditionMaps(t, stored, conditions) + }, + }, + { + name: "id0/latest + confId1/latest", + args: args{ + query: &pb.GetConditionsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConditionID(0), + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + ConfigurationIdFilter: []string{test.ConfigurationID(1)}, + }, + }, + want: func(t *testing.T, conditions map[string]store.Condition) { + stored := make(map[string]store.Condition) + for _, c := range conds { + if c.Id == test.ConditionID(0) || c.ConfigurationId == test.ConfigurationID(1) { + c.Versions = nil + stored[c.Id] = c + } + } + test.CmpStoredConditionMaps(t, stored, conditions) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conditions := make(map[string]store.Condition) + err := s.GetConditions(ctx, tt.args.owner, tt.args.query, func(c *store.Condition) error { + condition, ok := conditions[c.Id] + if ok { + errM := test.MergeConditions(&condition, c) + require.NoError(t, errM) + conditions[c.Id] = condition + return nil + } + conditions[c.Id] = *c.Clone() + return nil + }) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tt.want(t, conditions) + }) + } +} diff --git a/snippet-service/store/mongodb/getConfigurations_test.go b/snippet-service/store/mongodb/getConfigurations_test.go new file mode 100644 index 000000000..0ef510068 --- /dev/null +++ b/snippet-service/store/mongodb/getConfigurations_test.go @@ -0,0 +1,345 @@ +package mongodb_test + +import ( + "cmp" + "context" + "slices" + "testing" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestStoreGetConfigurations(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + confs := test.AddConfigurationsToStore(ctx, t, s, 500, nil) + + type args struct { + owner string + query *pb.GetConfigurationsRequest + } + tests := []struct { + name string + args args + wantErr bool + want func(t *testing.T, configurations map[string]*store.Configuration) + }{ + { + name: "all", + args: args{}, + want: func(t *testing.T, configurations map[string]*store.Configuration) { + require.Len(t, configurations, len(confs)) + for _, c := range configurations { + conf, ok := confs[c.Id] + require.True(t, ok) + test.CmpStoredConfiguration(t, &conf, c, false, true) + } + }, + }, + { + name: "owner0", + args: args{ + owner: test.Owner(0), + }, + want: func(t *testing.T, configurations map[string]*store.Configuration) { + require.NotEmpty(t, configurations) + for _, c := range configurations { + require.Equal(t, test.Owner(0), c.Owner) + conf, ok := confs[c.Id] + require.True(t, ok) + test.CmpStoredConfiguration(t, &conf, c, false, true) + } + }, + }, + { + name: "id1/all", + args: args{ + query: &pb.GetConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConfigurationID(1), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, configurations map[string]*store.Configuration) { + require.Len(t, configurations, 1) + c, ok := configurations[test.ConfigurationID(1)] + require.True(t, ok) + conf, ok := confs[c.Id] + require.True(t, ok) + test.CmpStoredConfiguration(t, &conf, c, false, true) + }, + }, + { + name: "owner2/id2/all", + args: args{ + owner: test.Owner(2), + query: &pb.GetConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConfigurationID(2), + Version: &pb.IDFilter_All{ + All: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, configurations map[string]*store.Configuration) { + require.Len(t, configurations, 1) + c, ok := configurations[test.ConfigurationID(2)] + require.True(t, ok) + conf, ok := confs[c.Id] + require.True(t, ok) + require.Equal(t, test.ConfigurationID(2), conf.Id) + require.Equal(t, test.Owner(2), conf.Owner) + test.CmpStoredConfiguration(t, &conf, c, false, true) + }, + }, + { + name: "latest", + args: args{ + query: &pb.GetConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, configurations map[string]*store.Configuration) { + require.Len(t, configurations, test.RuntimeConfig.NumConfigurations) + for _, c := range configurations { + conf, ok := confs[c.Id] + require.True(t, ok) + require.Equal(t, conf.Id, c.Id) + require.Equal(t, conf.Owner, c.Owner) + require.Empty(t, c.Versions) + test.CmpJSON(t, conf.Latest, c.Latest) + } + }, + }, + { + name: "owner1/latest", + args: args{ + owner: test.Owner(1), + query: &pb.GetConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, configurations map[string]*store.Configuration) { + require.Len(t, configurations, 3) + for _, c := range configurations { + conf, ok := confs[c.Id] + require.True(t, ok) + require.Equal(t, test.Owner(1), conf.Owner) + require.Empty(t, c.Versions) + test.CmpJSON(t, conf.Latest, c.Latest) + } + }, + }, + { + name: "owner2/id1/latest - non-matching owner", + args: args{ + owner: test.Owner(2), + query: &pb.GetConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Id: test.ConfigurationID(1), + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, configurations map[string]*store.Configuration) { + require.Empty(t, configurations) + }, + }, + { + name: "owner2{latest, id2/latest, id5/latest}", args: args{ + owner: test.Owner(2), + query: &pb.GetConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + { + Id: test.ConfigurationID(2), + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + { + Id: test.ConfigurationID(5), + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }, + }, + }, + }, + want: func(t *testing.T, configurations map[string]*store.Configuration) { + require.Len(t, configurations, 3) + for _, c := range configurations { + conf, ok := confs[c.Id] + require.True(t, ok) + require.Equal(t, test.Owner(2), conf.Owner) + require.Empty(t, c.Versions) + test.CmpJSON(t, conf.Latest, c.Latest) + } + }, + }, + { + name: "version/42", + args: args{ + query: &pb.GetConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Value{ + Value: 42, + }, + }, + }, + }, + }, + want: func(t *testing.T, configurations map[string]*store.Configuration) { + require.Len(t, configurations, 10) + for _, c := range configurations { + _, ok := confs[c.Id] + require.True(t, ok) + require.Len(t, c.Versions, 1) + require.Equal(t, uint64(42), c.Versions[0].Version) + } + }, + }, + { + name: "owner2/version/{13, 37, 42}", args: args{ + owner: test.Owner(2), + query: &pb.GetConfigurationsRequest{ + IdFilter: []*pb.IDFilter{ + { + Version: &pb.IDFilter_Value{ + Value: 13, + }, + }, + { + Version: &pb.IDFilter_Value{ + Value: 37, + }, + }, + { + Version: &pb.IDFilter_Value{ + Value: 42, + }, + }, + // duplicates should be ignored + { + Version: &pb.IDFilter_Value{ + Value: 37, + }, + }, + // filter with Id should be ignored if there are filters without Id + { + Id: test.ConfigurationID(2), + Version: &pb.IDFilter_Value{ + Value: 37, + }, + }, + }, + }, + }, + want: func(t *testing.T, configurations map[string]*store.Configuration) { + require.Len(t, configurations, 3) + for _, c := range configurations { + _, ok := confs[c.Id] + require.True(t, ok) + require.Len(t, c.Versions, 3) + slices.SortFunc(c.Versions, func(i, j store.ConfigurationVersion) int { + return cmp.Compare(i.Version, j.Version) + }) + require.Equal(t, uint64(13), c.Versions[0].Version) + require.Equal(t, uint64(37), c.Versions[1].Version) + require.Equal(t, uint64(42), c.Versions[2].Version) + } + }, + }, + { + name: "id0/version/{1..max} + latest", + args: args{ + query: &pb.GetConfigurationsRequest{ + IdFilter: func() []*pb.IDFilter { + var idFilters []*pb.IDFilter + c := confs[test.ConfigurationID(0)] + for _, v := range c.Versions { + idFilters = append(idFilters, &pb.IDFilter{ + Id: test.ConfigurationID(0), + Version: &pb.IDFilter_Value{ + Value: v.Version, + }, + }) + } + idFilters = append(idFilters, &pb.IDFilter{ + Id: test.ConfigurationID(0), + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }) + return idFilters + }(), + }, + }, + want: func(t *testing.T, configurations map[string]*store.Configuration) { + require.Len(t, configurations, 1) + for _, c := range configurations { + conf, ok := confs[c.Id] + require.True(t, ok) + require.Equal(t, test.ConfigurationID(0), conf.Id) + test.CmpStoredConfiguration(t, &conf, c, true, false) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + configurations := make(map[string]*store.Configuration) + err := s.GetConfigurations(ctx, tt.args.owner, tt.args.query, func(c *store.Configuration) error { + configuration, ok := configurations[c.Id] + if ok { + return test.MergeConfigurations(configuration, c) + } + configurations[c.Id] = c.Clone() + return nil + }) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tt.want(t, configurations) + }) + } +} diff --git a/snippet-service/store/mongodb/getLatestConditions_test.go b/snippet-service/store/mongodb/getLatestConditions_test.go new file mode 100644 index 000000000..aca4300b9 --- /dev/null +++ b/snippet-service/store/mongodb/getLatestConditions_test.go @@ -0,0 +1,309 @@ +package mongodb_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestStoreGetLatestConditions(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + const deviceID1 = "deviceID1" + const deviceID2 = "deviceID2" + const deviceID3 = "deviceID3" + const href1 = "/1" + const href2 = "/2" + const href3 = "/3" + const href4 = "/4" + const href5 = "/5" + const type1 = "type1" + const type2 = "type2" + const type3 = "type3" + const owner1 = "owner1" + const owner2 = "owner2" + cond1In := &pb.Condition{ + Id: uuid.NewString(), + Name: "c1", + Enabled: true, + ConfigurationId: uuid.NewString(), + Owner: owner1, + JqExpressionFilter: ".test", + } + cond1, err := s.CreateCondition(ctx, cond1In) + require.NoError(t, err) + + cond2In := &pb.Condition{ + Id: uuid.NewString(), + Name: "c2", + Enabled: true, + ConfigurationId: uuid.NewString(), + DeviceIdFilter: []string{deviceID1}, + ResourceHrefFilter: []string{href1, href2, href3}, + ResourceTypeFilter: []string{type1, type2}, + Owner: owner1, + } + cond2, err := s.CreateCondition(ctx, cond2In) + require.NoError(t, err) + + cond3In := &pb.Condition{ + Id: uuid.NewString(), + Name: "c3", + Enabled: true, + ConfigurationId: uuid.NewString(), + DeviceIdFilter: []string{deviceID2}, + ResourceHrefFilter: []string{href3, href4, href5}, + ResourceTypeFilter: []string{type3}, + Owner: owner1, + } + cond3, err := s.CreateCondition(ctx, cond3In) + require.NoError(t, err) + + cond4In := &pb.Condition{ + Id: uuid.NewString(), + Name: "c4", + Enabled: true, + ConfigurationId: uuid.NewString(), + DeviceIdFilter: []string{deviceID1, deviceID3}, + ResourceHrefFilter: []string{href1, href5}, + ResourceTypeFilter: []string{type3}, + Owner: owner1, + } + cond4, err := s.CreateCondition(ctx, cond4In) + require.NoError(t, err) + + cond5In := &pb.Condition{ + Id: uuid.NewString(), + Name: "c5", + Enabled: true, + ConfigurationId: uuid.NewString(), + DeviceIdFilter: []string{deviceID3}, + ResourceHrefFilter: []string{href1, href2}, + ResourceTypeFilter: []string{type2}, + Owner: owner2, + } + cond5, err := s.CreateCondition(ctx, cond5In) + require.NoError(t, err) + + cond6In := &pb.Condition{ + Id: uuid.NewString(), + Name: "c6", + Enabled: true, + ConfigurationId: uuid.NewString(), + DeviceIdFilter: []string{deviceID2, deviceID3}, + ResourceHrefFilter: []string{href2, href3, href4}, + ResourceTypeFilter: []string{type1, type2, type3}, + Owner: owner2, + } + cond6, err := s.CreateCondition(ctx, cond6In) + require.NoError(t, err) + + cond7In := &pb.Condition{ + Id: uuid.NewString(), + Name: "c7", + Enabled: true, + ConfigurationId: uuid.NewString(), + DeviceIdFilter: []string{deviceID3}, + Owner: owner2, + } + cond7, err := s.CreateCondition(ctx, cond7In) + require.NoError(t, err) + + cond8In := &pb.Condition{ + Id: uuid.NewString(), + Name: "c8 - disabled", + Enabled: false, + ConfigurationId: uuid.NewString(), + DeviceIdFilter: []string{deviceID2, deviceID3}, + ResourceHrefFilter: []string{href2, href3, href4}, + ResourceTypeFilter: []string{type1, type2, type3}, + Owner: owner2, + } + _, err = s.CreateCondition(ctx, cond8In) + require.NoError(t, err) + + type args struct { + query *store.GetLatestConditionsQuery + owner string + } + + tests := []struct { + name string + args args + wantErr bool + want []*pb.Condition + }{ + { + name: "invalid query", + args: args{ + query: &store.GetLatestConditionsQuery{}, + }, + wantErr: true, + }, + { + name: "deviceID3", + args: args{ + query: &store.GetLatestConditionsQuery{ + DeviceID: deviceID3, + }, + }, + want: []*pb.Condition{cond1, cond4, cond5, cond6, cond7}, + }, + { + name: "owner1/deviceID1", + args: args{ + query: &store.GetLatestConditionsQuery{ + DeviceID: deviceID1, + }, + owner: owner1, + }, + want: []*pb.Condition{cond1, cond2, cond4}, + }, + { + name: "owner1/deviceID2", + args: args{ + query: &store.GetLatestConditionsQuery{ + DeviceID: deviceID2, + }, + owner: owner1, + }, + want: []*pb.Condition{cond1, cond3}, + }, + { + name: "owner1/new deviceID", + args: args{ + query: &store.GetLatestConditionsQuery{ + DeviceID: uuid.NewString(), + }, + owner: owner1, + }, + want: []*pb.Condition{cond1}, + }, + { + name: "owner2/deviceID1", + args: args{ + query: &store.GetLatestConditionsQuery{ + DeviceID: deviceID1, + }, + owner: owner2, + }, + want: []*pb.Condition{}, + }, + { + name: "owner2/deviceID3", + args: args{ + query: &store.GetLatestConditionsQuery{ + DeviceID: deviceID3, + }, + owner: owner2, + }, + want: []*pb.Condition{cond5, cond6, cond7}, + }, + { + name: "href1", + args: args{ + query: &store.GetLatestConditionsQuery{ + ResourceHref: href1, + }, + }, + want: []*pb.Condition{cond1, cond2, cond4, cond5, cond7}, + }, + { + name: "deviceID1/href3", + args: args{ + query: &store.GetLatestConditionsQuery{ + DeviceID: deviceID1, + ResourceHref: href3, + }, + }, + want: []*pb.Condition{cond1, cond2}, + }, + { + name: "owner2/href2", + args: args{ + query: &store.GetLatestConditionsQuery{ + ResourceHref: href2, + }, + owner: owner2, + }, + want: []*pb.Condition{cond5, cond6, cond7}, + }, + { + name: "[type2]", + args: args{ + query: &store.GetLatestConditionsQuery{ + ResourceTypeFilter: []string{type2}, + }, + }, + want: []*pb.Condition{cond1, cond2, cond5, cond6, cond7}, + }, + { + name: "deviceID2/[type3]", + args: args{ + query: &store.GetLatestConditionsQuery{ + DeviceID: deviceID2, + ResourceTypeFilter: []string{type3}, + }, + }, + want: []*pb.Condition{cond1, cond3, cond6}, + }, + { + name: "owner2/[type1,type2,type3}", + args: args{ + query: &store.GetLatestConditionsQuery{ + // order should not matter + ResourceTypeFilter: []string{type2, type1, type3}, + }, + owner: owner2, + }, + want: []*pb.Condition{cond6, cond7}, + }, + { + name: "deviceID1/href5/[type3]", + args: args{ + query: &store.GetLatestConditionsQuery{ + DeviceID: deviceID1, + ResourceHref: href5, + ResourceTypeFilter: []string{type3}, + }, + }, + want: []*pb.Condition{cond1, cond4}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conditions := make(map[string]*pb.Condition) + err := s.GetLatestEnabledConditions(ctx, tt.args.owner, tt.args.query, func(c *store.Condition) error { + condition, errG := c.GetLatest() + if errG != nil { + return errG + } + conditions[c.Id] = condition.Clone() + return nil + }) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + require.Len(t, conditions, len(tt.want)) + for _, c := range tt.want { + latest, ok := conditions[c.GetId()] + require.True(t, ok) + test.CmpJSON(t, latest, c) + } + }) + } +} diff --git a/snippet-service/store/mongodb/store.go b/snippet-service/store/mongodb/store.go new file mode 100644 index 000000000..901266a50 --- /dev/null +++ b/snippet-service/store/mongodb/store.go @@ -0,0 +1,77 @@ +package mongodb + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-multierror" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgMongo "github.com/plgd-dev/hub/v2/pkg/mongodb" + "github.com/plgd-dev/hub/v2/pkg/security/certManager/client" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.opentelemetry.io/otel/trace" +) + +type Store struct { + *pkgMongo.Store +} + +const ( + conditionsCol = "conditions" + configurationsCol = "configurations" + appliedConfigurationsCol = "appliedConfigurations" +) + +var idUniqueIndex = mongo.IndexModel{ + Keys: bson.D{ + {Key: pb.IDKey, Value: 1}, + }, + Options: options.Index().SetUnique(true), +} + +var deviceIDConfigurationIDUniqueIndex = mongo.IndexModel{ + Keys: bson.D{ + {Key: pb.DeviceIDKey, Value: 1}, + {Key: pb.ConfigurationLinkIDKey, Value: 1}, + }, + Options: options.Index().SetUnique(true), +} + +func New(ctx context.Context, cfg *Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Store, error) { + certManager, err := client.New(cfg.Mongo.TLS, fileWatcher, logger) + if err != nil { + return nil, fmt.Errorf("could not create cert manager: %w", err) + } + + m, err := pkgMongo.NewStoreWithCollections(ctx, &cfg.Mongo, certManager.GetTLSConfig(), tracerProvider, map[string][]mongo.IndexModel{ + conditionsCol: nil, + configurationsCol: nil, + appliedConfigurationsCol: {idUniqueIndex, deviceIDConfigurationIDUniqueIndex}, + }) + if err != nil { + certManager.Close() + return nil, err + } + s := Store{Store: m} + s.SetOnClear(s.clearDatabases) + s.AddCloseFunc(certManager.Close) + return &s, nil +} + +func (s *Store) clearDatabases(ctx context.Context) error { + var errors *multierror.Error + collections := []string{conditionsCol, configurationsCol, appliedConfigurationsCol} + for _, collection := range collections { + err := s.Collection(collection).Drop(ctx) + errors = multierror.Append(errors, err) + } + return errors.ErrorOrNil() +} + +func (s *Store) Close(ctx context.Context) error { + return s.Store.Close(ctx) +} diff --git a/snippet-service/store/mongodb/updateAppliedConfiguration_test.go b/snippet-service/store/mongodb/updateAppliedConfiguration_test.go new file mode 100644 index 000000000..c00953402 --- /dev/null +++ b/snippet-service/store/mongodb/updateAppliedConfiguration_test.go @@ -0,0 +1,152 @@ +package mongodb_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/resource-aggregate/events" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestStoreUpdateAppliedConfigurationResource(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + id := uuid.NewString() + confID := uuid.NewString() + condID := uuid.NewString() + resources := []*pb.AppliedConfiguration_Resource{ + { + Href: "/test/1", + CorrelationId: "corID1", + Status: pb.AppliedConfiguration_Resource_PENDING, + }, + { + Href: "/test/2", + CorrelationId: "corID2", + Status: pb.AppliedConfiguration_Resource_PENDING, + ResourceUpdated: &events.ResourceUpdated{ + ResourceId: &commands.ResourceId{DeviceId: "deviceID", Href: "/test/2"}, + }, + }, + { + Href: "/test/3", + CorrelationId: "corID3", + Status: pb.AppliedConfiguration_Resource_QUEUED, + }, + } + owner := "owner1" + appliedConf, _, err := s.CreateAppliedConfiguration(ctx, &pb.AppliedConfiguration{ + Id: id, + DeviceId: "dev1", + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{ + Id: confID, + }, + ExecutedBy: pb.MakeExecutedByConditionId(condID, 0), + Resources: resources, + Owner: owner, + }, false) + require.NoError(t, err) + + // error - missing applied configuration ID + _, err = s.UpdateAppliedConfigurationResource(ctx, owner, store.UpdateAppliedConfigurationResourceRequest{ + Resource: &pb.AppliedConfiguration_Resource{ + Href: "/test/1", + CorrelationId: "corID1", + Status: pb.AppliedConfiguration_Resource_DONE, + }, + }) + require.Error(t, err) + + // error - missing resource + _, err = s.UpdateAppliedConfigurationResource(ctx, owner, store.UpdateAppliedConfigurationResourceRequest{ + AppliedConfigurationID: id, + }) + require.Error(t, err) + + // error - missing resource href + _, err = s.UpdateAppliedConfigurationResource(ctx, owner, store.UpdateAppliedConfigurationResourceRequest{ + AppliedConfigurationID: id, + Resource: &pb.AppliedConfiguration_Resource{ + CorrelationId: "corID1", + Status: pb.AppliedConfiguration_Resource_DONE, + }, + }) + require.Error(t, err) + + // error - missing resource correlationID + _, err = s.UpdateAppliedConfigurationResource(ctx, owner, store.UpdateAppliedConfigurationResourceRequest{ + AppliedConfigurationID: id, + Resource: &pb.AppliedConfiguration_Resource{ + Href: "/test/1", + Status: pb.AppliedConfiguration_Resource_DONE, + }, + }) + require.Error(t, err) + + // error - invalid resource status + _, err = s.UpdateAppliedConfigurationResource(ctx, owner, store.UpdateAppliedConfigurationResourceRequest{ + AppliedConfigurationID: id, + Resource: &pb.AppliedConfiguration_Resource{ + Href: "/test/1", + CorrelationId: "corID1", + }, + }) + require.Error(t, err) + _, err = s.UpdateAppliedConfigurationResource(ctx, owner, store.UpdateAppliedConfigurationResourceRequest{ + AppliedConfigurationID: id, + Resource: &pb.AppliedConfiguration_Resource{ + Href: "/test/1", + CorrelationId: "corID1", + Status: pb.AppliedConfiguration_Resource_UNSPECIFIED, + }, + }) + require.Error(t, err) + + updatedAppliedConf, err := s.UpdateAppliedConfigurationResource(ctx, owner, store.UpdateAppliedConfigurationResourceRequest{ + AppliedConfigurationID: id, + StatusFilter: []pb.AppliedConfiguration_Resource_Status{pb.AppliedConfiguration_Resource_PENDING}, + Resource: &pb.AppliedConfiguration_Resource{ + Href: "/test/1", + CorrelationId: "corID1", + Status: pb.AppliedConfiguration_Resource_DONE, + }, + }) + require.NoError(t, err) + wantAppliedConf := appliedConf.Clone() + wantAppliedConf.Resources[0].Status = pb.AppliedConfiguration_Resource_DONE + test.CmpAppliedDeviceConfiguration(t, wantAppliedConf, updatedAppliedConf, true) + + // /test/1 is no longer in pending state, so additional update should fail + _, err = s.UpdateAppliedConfigurationResource(ctx, owner, store.UpdateAppliedConfigurationResourceRequest{ + AppliedConfigurationID: id, + StatusFilter: []pb.AppliedConfiguration_Resource_Status{pb.AppliedConfiguration_Resource_PENDING}, + Resource: &pb.AppliedConfiguration_Resource{ + Href: "/test/1", + CorrelationId: "corID1", + Status: pb.AppliedConfiguration_Resource_TIMEOUT, + }, + }) + require.ErrorIs(t, err, store.ErrNotModified) + + // mismatched owner + _, err = s.UpdateAppliedConfigurationResource(ctx, "mismatch", store.UpdateAppliedConfigurationResourceRequest{ + AppliedConfigurationID: id, + StatusFilter: []pb.AppliedConfiguration_Resource_Status{pb.AppliedConfiguration_Resource_PENDING}, + Resource: &pb.AppliedConfiguration_Resource{ + Href: "/test/2", + CorrelationId: "corID2", + Status: pb.AppliedConfiguration_Resource_DONE, + }, + }) + require.Error(t, err) +} diff --git a/snippet-service/store/mongodb/updateCondition_test.go b/snippet-service/store/mongodb/updateCondition_test.go new file mode 100644 index 000000000..1f28234b6 --- /dev/null +++ b/snippet-service/store/mongodb/updateCondition_test.go @@ -0,0 +1,160 @@ +package mongodb_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestStoreUpdateCondition(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + cond, err := s.CreateCondition(ctx, &pb.Condition{ + Id: uuid.NewString(), + Name: "update", + Enabled: true, + ConfigurationId: uuid.NewString(), + Owner: "owner1", + Version: 42, + }) + require.NoError(t, err) + + type args struct { + update *pb.Condition + } + tests := []struct { + name string + args args + want func(*pb.Condition) + wantErr bool + }{ + { + name: "missing Id", + args: args{ + update: &pb.Condition{ + Owner: cond.GetOwner(), + ConfigurationId: uuid.NewString(), + Version: 1, + }, + }, + wantErr: true, + }, + { + name: "missing ConfigurationId", + args: args{ + update: &pb.Condition{ + Id: uuid.NewString(), + Owner: cond.GetOwner(), + Version: 1, + }, + }, + wantErr: true, + }, + { + name: "non-matching owner", + args: args{ + update: &pb.Condition{ + Id: cond.GetId(), + Owner: "invalid", + ConfigurationId: cond.GetConfigurationId(), + Version: 1, + }, + }, + wantErr: true, + }, + { + name: "duplicit version", + args: args{ + update: &pb.Condition{ + Id: cond.GetId(), + Owner: cond.GetOwner(), + ConfigurationId: cond.GetConfigurationId(), + Version: cond.GetVersion(), + }, + }, + wantErr: true, + }, + { + name: "invalid version", + args: args{ + update: &pb.Condition{ + Id: cond.GetId(), + Owner: cond.GetOwner(), + ConfigurationId: cond.GetConfigurationId(), + Version: cond.GetVersion() - 1, // version must be higher than the latest one + }, + }, + wantErr: true, + }, + { + name: "update", + args: args{ + update: &pb.Condition{ + Id: cond.GetId(), + ConfigurationId: cond.GetConfigurationId(), + Owner: cond.GetOwner(), + Version: 43, + Name: "updated name", + Enabled: false, + ApiAccessToken: "updated token", + DeviceIdFilter: []string{"device2", "device1", "device1"}, + ResourceTypeFilter: []string{"plgd.test", "plgd.test"}, + ResourceHrefFilter: []string{"/test/2", "/test/1", "/test/2"}, + JqExpressionFilter: "{}", + }, + }, + want: func(updated *pb.Condition) { + require.Equal(t, "updated name", updated.GetName()) + require.False(t, updated.GetEnabled()) + require.Equal(t, "updated token", updated.GetApiAccessToken()) + require.ElementsMatch(t, []string{"device1", "device2"}, updated.GetDeviceIdFilter()) + require.ElementsMatch(t, []string{"plgd.test"}, updated.GetResourceTypeFilter()) + require.ElementsMatch(t, []string{"/test/1", "/test/2"}, updated.GetResourceHrefFilter()) + require.Equal(t, "{}", updated.GetJqExpressionFilter()) + }, + }, + { + name: "second update", + args: args{ + update: &pb.Condition{ + Id: cond.GetId(), + ConfigurationId: cond.GetConfigurationId(), + Owner: cond.GetOwner(), + Name: "next updated name", + Enabled: true, + ApiAccessToken: "next updated token", + }, + }, + want: func(updated *pb.Condition) { + require.Equal(t, "next updated name", updated.GetName()) + require.True(t, updated.GetEnabled()) + require.Equal(t, "next updated token", updated.GetApiAccessToken()) + require.Empty(t, updated.GetDeviceIdFilter()) + require.Empty(t, updated.GetResourceTypeFilter()) + require.Empty(t, updated.GetResourceHrefFilter()) + require.Empty(t, updated.GetJqExpressionFilter()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cond, err := s.UpdateCondition(ctx, tt.args.update) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tt.want(cond) + }) + } +} diff --git a/snippet-service/store/mongodb/updateConfiguration_test.go b/snippet-service/store/mongodb/updateConfiguration_test.go new file mode 100644 index 000000000..4879b56c9 --- /dev/null +++ b/snippet-service/store/mongodb/updateConfiguration_test.go @@ -0,0 +1,123 @@ +package mongodb_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestStoreUpdateConfiguration(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + confID := uuid.NewString() + const owner = "owner1" + resources := []*pb.Configuration_Resource{ + makeLightResourceConfiguration(t, "1", 1, 1337), + } + conf, err := s.CreateConfiguration(ctx, &pb.Configuration{ + Id: confID, + Name: "valid", + Owner: owner, + Version: 13, + Resources: resources, + }) + require.NoError(t, err) + + type args struct { + update *pb.Configuration + } + tests := []struct { + name string + args args + wantErr bool + want func(*testing.T, *pb.Configuration) + }{ + { + name: "non-matching owner", + args: args{ + update: &pb.Configuration{ + Id: conf.GetId(), + Owner: "invalid", + Version: 0, + Resources: resources, + }, + }, + wantErr: true, + }, + { + name: "duplicit version", + args: args{ + update: &pb.Configuration{ + Id: conf.GetId(), + Owner: conf.GetOwner(), + Version: 13, + Resources: resources, + }, + }, + wantErr: true, + }, + { + name: "invalid version", + args: args{ + update: &pb.Configuration{ + Id: conf.GetId(), + Owner: conf.GetOwner(), + Version: conf.GetVersion() - 1, // version must be higher than the latest one + Resources: resources, + }, + }, + wantErr: true, + }, + { + name: "missing ID", + args: args{ + update: &pb.Configuration{ + Owner: conf.GetOwner(), + Version: 1, + Resources: resources, + }, + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + update: &pb.Configuration{ + Id: conf.GetId(), + Name: "updated name", + Owner: conf.GetOwner(), + Resources: []*pb.Configuration_Resource{ + makeLightResourceConfiguration(t, "2", 2, 42), + }, + }, + }, + want: func(t *testing.T, updatedConf *pb.Configuration) { + require.Equal(t, conf.GetId(), updatedConf.GetId()) + require.Equal(t, "updated name", updatedConf.GetName()) + require.Equal(t, conf.GetOwner(), updatedConf.GetOwner()) + require.Equal(t, conf.GetVersion()+1, updatedConf.GetVersion()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf, err := s.UpdateConfiguration(ctx, tt.args.update) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + tt.want(t, conf) + }) + } +} diff --git a/snippet-service/store/mongodb/utility.go b/snippet-service/store/mongodb/utility.go new file mode 100644 index 000000000..3ce936aaa --- /dev/null +++ b/snippet-service/store/mongodb/utility.go @@ -0,0 +1,229 @@ +package mongodb + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-multierror" + "github.com/plgd-dev/hub/v2/pkg/mongodb" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +const ( + temporaryLatestKey = "__latest" +) + +func addMatchCondition(owner string, id string, notEmpty bool) bson.D { + match := bson.D{ + {Key: pb.VersionsKey + ".0", Value: bson.M{mongodb.Exists: notEmpty}}, + } + if id != "" { + match = append(match, bson.E{Key: pb.RecordIDKey, Value: id}) + } + if owner != "" { + match = append(match, bson.E{Key: pb.OwnerKey, Value: owner}) + } + return match +} + +func appendLatestToVersions() bson.M { + return bson.M{ + "$concatArrays": bson.A{ + bson.M{ + "$ifNull": bson.A{ + "$" + pb.VersionsKey, + bson.A{}, + }, + }, + bson.A{"$" + pb.LatestKey}, + }, + } +} + +func incrementLatestVersion(key string) bson.M { + return bson.M{ + key: bson.M{ + "$add": bson.A{ + bson.M{"$ifNull": bson.A{"$" + pb.LatestKey + "." + pb.VersionKey, 0}}, + 1, + }, + }, + } +} + +func getVersionsPipeline(pl mongo.Pipeline, versions []uint64, latest, exclude bool) mongo.Pipeline { + vfilter := make([]interface{}, 0, len(versions)+1) + for _, version := range versions { + vfilter = append(vfilter, version) + } + if latest { + vfilter = append(vfilter, "$"+pb.LatestKey+"."+pb.VersionKey) + } + if len(vfilter) == 0 { + return pl + } + cond := bson.M{mongodb.In: bson.A{"$$version." + pb.VersionKey, vfilter}} + if exclude { + cond = bson.M{"$not": cond} + } + pl = append(pl, bson.D{{Key: "$addFields", Value: bson.M{ + pb.VersionsKey: bson.M{ + "$filter": bson.M{ + "input": "$" + pb.VersionsKey, + "as": "version", + "cond": cond, + }, + }, + }}}) + return pl +} + +func getPipeline(owner, id string, versions []uint64) mongo.Pipeline { + pl := mongo.Pipeline{bson.D{{Key: mongodb.Match, Value: addMatchCondition(owner, id, true)}}} + project := bson.M{ + pb.LatestKey: false, + } + pl = getVersionsPipeline(pl, versions, false, false) + pl = append(pl, bson.D{{Key: "$project", Value: project}}) + return pl +} + +func inArrayQuery(key string, values []string) bson.M { + filter := bson.A{} + for _, v := range values { + if v == "" { + continue + } + filter = append(filter, v) + } + if len(filter) == 0 { + return nil + } + return bson.M{key: bson.D{{Key: mongodb.In, Value: filter}}} +} + +func toIdQuery(ids []string) bson.M { + return inArrayQuery(pb.RecordIDKey, ids) +} + +func toFilter(op string, filters []interface{}) interface{} { + if len(filters) == 0 { + return nil + } + if len(filters) == 1 { + return filters[0] + } + return bson.M{op: filters} +} + +func toFilterQuery(op string, filters []interface{}) interface{} { + filter := toFilter(op, filters) + if filter == nil { + return bson.M{} + } + return filter +} + +func toIdFilterQuery(owner string, idfilter bson.M, emptyVersions bool) interface{} { + filters := make([]interface{}, 0, 3) + if owner != "" { + filters = append(filters, bson.D{{Key: pb.OwnerKey, Value: owner}}) + } + if idfilter != nil { + filters = append(filters, idfilter) + } + if emptyVersions { + filters = append(filters, bson.D{{Key: pb.VersionsKey + ".0", Value: bson.M{mongodb.Exists: false}}}) + } + return toFilterQuery(mongodb.And, filters) +} + +func processCursor[T any](ctx context.Context, cr *mongo.Cursor, process store.Process[T]) error { + var errors *multierror.Error + iter := store.MongoIterator[T]{ + Cursor: cr, + } + for { + var stored T + if !iter.Next(ctx, &stored) { + break + } + err := process(&stored) + if err != nil { + errors = multierror.Append(errors, err) + break + } + } + errors = multierror.Append(errors, iter.Err()) + errClose := cr.Close(ctx) + errors = multierror.Append(errors, errClose) + return errors.ErrorOrNil() +} + +func toDeleteResult(err error, partialSuccess bool) error { + if err != nil { + if partialSuccess { + return fmt.Errorf("%w: %w", store.ErrPartialDelete, err) + } + return err + } + return nil +} + +func (s *Store) deleteVersion(ctx context.Context, collection, owner string, id string, versions []uint64) error { + pl := getVersionsPipeline(mongo.Pipeline{}, versions, false, true) + // take last element from versions array as latest (if it exists) + pl = append(pl, bson.D{{Key: mongodb.Set, Value: bson.M{ + pb.LatestKey: bson.D{{Key: "$arrayElemAt", Value: bson.A{"$" + pb.VersionsKey, -1}}}, + }}}) + _, err := s.Collection(collection).UpdateMany(ctx, toIdFilterQuery(owner, toIdQuery([]string{id}), false), pl) + return err +} + +func (s *Store) deleteLatestVersion(ctx context.Context, collection, owner string, ids []string) error { + pl := getVersionsPipeline(mongo.Pipeline{}, nil, true, true) + // take last element from versions array as latest (if it exists) + pl = append(pl, bson.D{{Key: mongodb.Set, Value: bson.M{ + pb.LatestKey: bson.D{{Key: "$arrayElemAt", Value: bson.A{"$" + pb.VersionsKey, -1}}}, + }}}) + _, err := s.Collection(collection).UpdateMany(ctx, toIdFilterQuery(owner, toIdQuery(ids), false), pl) + return err +} + +func (s *Store) deleteDocuments(ctx context.Context, collection, owner string, ids []string) error { + _, err := s.Collection(collection).DeleteMany(ctx, toIdFilterQuery(owner, toIdQuery(ids), false)) + return err +} + +func (s *Store) delete(ctx context.Context, collection, owner string, idfilter []*pb.IDFilter) error { + success := false + vf := pb.PartitionIDFilter(idfilter) + var errors *multierror.Error + if len(vf.All) > 0 || vf.IsEmpty() { + err := s.deleteDocuments(ctx, collection, owner, vf.All) + success = success || err == nil + errors = multierror.Append(errors, err) + } + + if len(vf.Latest) > 0 { + err := s.deleteLatestVersion(ctx, collection, owner, vf.Latest) + success = success || err == nil + errors = multierror.Append(errors, err) + } + + for id, versions := range vf.Versions { + err := s.deleteVersion(ctx, collection, owner, id, versions) + success = success || err == nil + errors = multierror.Append(errors, err) + } + + // delete documents with empty versions + if len(vf.Latest) > 0 || len(vf.Versions) > 0 { + _, err := s.Collection(collection).DeleteMany(ctx, toIdFilterQuery(owner, nil, true)) + errors = multierror.Append(errors, err) + } + return toDeleteResult(errors.ErrorOrNil(), success) +} diff --git a/snippet-service/store/store.go b/snippet-service/store/store.go new file mode 100644 index 000000000..6d90f3bb4 --- /dev/null +++ b/snippet-service/store/store.go @@ -0,0 +1,109 @@ +package store + +import ( + "context" + "errors" + "fmt" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "go.mongodb.org/mongo-driver/mongo" +) + +type ( + GetLatestConditionsQuery struct { + DeviceID string + ResourceHref string + ResourceTypeFilter []string + } +) + +type Iterator[T any] interface { + Next(ctx context.Context, v *T) bool + Err() error +} + +type ( + Process[T any] func(v *T) error + ProccessAppliedConfigurations = Process[AppliedConfiguration] + ProcessConfigurations = Process[Configuration] + ProcessConditions = Process[Condition] +) + +var ( + ErrNotSupported = errors.New("not supported") + ErrNotFound = errors.New("not found") + ErrNotModified = errors.New("not modified") + ErrInvalidArgument = errors.New("invalid argument") + ErrPartialDelete = errors.New("some errors occurred while deleting") +) + +func errInvalidArgument(err error) error { + return fmt.Errorf("%w: %w", ErrInvalidArgument, err) +} + +func IsDuplicateKeyError(err error) bool { + return mongo.IsDuplicateKeyError(err) +} + +type MongoIterator[T any] struct { + Cursor *mongo.Cursor +} + +func (i *MongoIterator[T]) Next(ctx context.Context, s *T) bool { + if !i.Cursor.Next(ctx) { + return false + } + err := i.Cursor.Decode(s) + return err == nil +} + +func (i *MongoIterator[T]) Err() error { + return i.Cursor.Err() +} + +type Store interface { + // CreateCondition creates a new condition. If the condition already exists, it will throw an error. + CreateCondition(ctx context.Context, condition *pb.Condition) (*pb.Condition, error) + // UpdateCondition updates an existing condition. + UpdateCondition(ctx context.Context, condition *pb.Condition) (*pb.Condition, error) + // GetConditions loads conditions from the database. + GetConditions(ctx context.Context, owner string, query *pb.GetConditionsRequest, p ProcessConditions) error + // DeleteConditions deletes conditions from the database. + DeleteConditions(ctx context.Context, owner string, query *pb.DeleteConditionsRequest) error + // InsertConditions inserts conditions into the database. + InsertConditions(ctx context.Context, conditions ...*Condition) error + // GetLatestEnabledConditions finds latest conditions that match the query. + GetLatestEnabledConditions(ctx context.Context, owner string, query *GetLatestConditionsQuery, p ProcessConditions) error + + // CreateConfiguration creates a new configuration in the database. + CreateConfiguration(ctx context.Context, conf *pb.Configuration) (*pb.Configuration, error) + // UpdateConfiguration updates an existing configuration in the database. + UpdateConfiguration(ctx context.Context, conf *pb.Configuration) (*pb.Configuration, error) + // GetConfigurations loads a configurations from the database. + GetConfigurations(ctx context.Context, owner string, query *pb.GetConfigurationsRequest, p ProcessConfigurations) error + // DeleteConfigurations deletes configurations from the database. + DeleteConfigurations(ctx context.Context, owner string, query *pb.DeleteConfigurationsRequest) error + // InsertConditions inserts conditions into the database. + InsertConfigurations(ctx context.Context, configurations ...*Configuration) error + // GetLatestConfigurationsByID finds latest configurations by their IDs. + GetLatestConfigurationsByID(ctx context.Context, owner string, ids []string, p ProcessConfigurations) error + + // GetAppliedConfigurations loads applied device configurations from the database. + GetAppliedConfigurations(ctx context.Context, owner string, query *pb.GetAppliedConfigurationsRequest, p ProccessAppliedConfigurations) error + // DeleteAppliedConfigurations deletes applied device configurations from the database. + DeleteAppliedConfigurations(ctx context.Context, owner string, query *pb.DeleteAppliedConfigurationsRequest) error + // CreateAppliedConfiguration creates a new applied device configuration in the database. + // + // If the configuration with given deviceID and configurationID already exists, it will throw an error, unless the force flag is set to true. + // + // The first return value is the created applied device configuration. The second return value is the applied device configuration that was replaced if the force flag was set to true. + CreateAppliedConfiguration(ctx context.Context, conf *pb.AppliedConfiguration, force bool) (*pb.AppliedConfiguration, *pb.AppliedConfiguration, error) + // InsertAppliedConditions inserts applied configurations into the database. + InsertAppliedConfigurations(ctx context.Context, configurations ...*AppliedConfiguration) error + // UpdateAppliedConfigurationResource updates an existing applied device configuration resource in the database. + UpdateAppliedConfigurationResource(ctx context.Context, owner string, query UpdateAppliedConfigurationResourceRequest) (*pb.AppliedConfiguration, error) + // GetPendingAppliedConfigurationResourceUpdates loads applied device configuration with expired (validUntil <= now) resource updates from the database. + GetPendingAppliedConfigurationResourceUpdates(ctx context.Context, expiredOnly bool, p ProccessAppliedConfigurations) (int64, error) + + Close(ctx context.Context) error +} diff --git a/snippet-service/test/appliedConfiguration.go b/snippet-service/test/appliedConfiguration.go new file mode 100644 index 000000000..e913fb758 --- /dev/null +++ b/snippet-service/test/appliedConfiguration.go @@ -0,0 +1,160 @@ +package test + +import ( + "context" + "errors" + "io" + "slices" + "strconv" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + hubTest "github.com/plgd-dev/hub/v2/test" + pbTest "github.com/plgd-dev/hub/v2/test/pb" + "github.com/stretchr/testify/require" +) + +func DeviceID(i int) string { + return "device" + strconv.Itoa(i) +} + +func AppliedConfigurationID(i int) string { + if id, ok := RuntimeConfig.appliedConfigurationIds[i]; ok { + return id + } + id := uuid.NewString() + RuntimeConfig.appliedConfigurationIds[i] = id + return id +} + +func SetAppliedConfigurationExecutedBy(ac *pb.AppliedConfiguration, i int) { + if i%RuntimeConfig.NumConfigurations == 0 { + ac.ExecutedBy = pb.MakeExecutedByOnDemand() + return + } + ac.ExecutedBy = pb.MakeExecutedByConditionId(ConditionID(i), uint64(i%RuntimeConfig.NumConditions)) +} + +func AppliedConfigurationResource(t *testing.T, deviceID string, start, n int) []*pb.AppliedConfiguration_Resource { + resources := make([]*pb.AppliedConfiguration_Resource, 0, n) + for i := start; i < start+n; i++ { + correlationID := "corID" + strconv.Itoa(i) + resource := &pb.AppliedConfiguration_Resource{ + Href: hubTest.TestResourceLightInstanceHref(strconv.Itoa(i)), + CorrelationId: correlationID, + Status: pb.AppliedConfiguration_Resource_Status(1 + i%4), + } + if resource.GetStatus() == pb.AppliedConfiguration_Resource_PENDING { + resource.ValidUntil = time.Now().Add(time.Minute * -3).Add(time.Minute * time.Duration(i)).UnixNano() + } + if resource.GetStatus() == pb.AppliedConfiguration_Resource_DONE { + resource.ResourceUpdated = pbTest.MakeResourceUpdated(t, + deviceID, + resource.GetHref(), + hubTest.TestResourceLightInstanceResourceTypes, + correlationID, + map[string]interface{}{ + "power": i, + }, + ) + } + resources = append(resources, resource) + } + return resources +} + +func getAppliedConfigurations(t *testing.T) map[string]*store.AppliedConfiguration { + owners := make(map[int]string, RuntimeConfig.NumConfigurations) + acs := make(map[string]*store.AppliedConfiguration) + i := 0 + for d := range RuntimeConfig.numDevices { + for c := range RuntimeConfig.NumConfigurations { + owner, ok := owners[i%RuntimeConfig.NumConfigurations] + if !ok { + owner = Owner(i % RuntimeConfig.NumOwners) + owners[i%RuntimeConfig.NumConfigurations] = owner + } + deviceID := DeviceID(d) + ac := store.MakeAppliedConfiguration(&pb.AppliedConfiguration{ + Id: AppliedConfigurationID(i), + DeviceId: deviceID, + Owner: owner, + ConfigurationId: &pb.AppliedConfiguration_LinkedTo{ + Id: ConfigurationID(c), + Version: uint64(i % RuntimeConfig.NumConfigurations), + }, + Resources: AppliedConfigurationResource(t, deviceID, i%16, (i%5)+1), + Timestamp: time.Now().UnixNano(), + }) + SetAppliedConfigurationExecutedBy(ac.GetAppliedConfiguration(), i) + acs[ac.GetId()] = &ac + i++ + } + } + return acs +} + +func AddAppliedConfigurationsToStore(ctx context.Context, t *testing.T, s store.Store) map[string]*store.AppliedConfiguration { + acs := getAppliedConfigurations(t) + acsToInsert := make([]*store.AppliedConfiguration, 0, len(acs)) + for _, c := range acs { + acsToInsert = append(acsToInsert, c) + } + err := s.InsertAppliedConfigurations(ctx, acsToInsert...) + require.NoError(t, err) + return acs +} + +func GetAppliedConfigurations(ctx context.Context, t *testing.T, snippetClient pb.SnippetServiceClient, req *pb.GetAppliedConfigurationsRequest) (map[string]*pb.AppliedConfiguration, map[string]*pb.AppliedConfiguration_Resource) { + getClient, errG := snippetClient.GetAppliedConfigurations(ctx, req) + require.NoError(t, errG) + defer func() { + _ = getClient.CloseSend() + }() + appliedConfs := make(map[string]*pb.AppliedConfiguration) + for { + conf, errR := getClient.Recv() + if errors.Is(errR, io.EOF) { + break + } + require.NoError(t, errR) + appliedConfs[conf.GetId()] = conf + } + appliedConfResources := make(map[string]*pb.AppliedConfiguration_Resource) + for _, appliedConf := range appliedConfs { + for _, r := range appliedConf.GetResources() { + id := appliedConf.GetConfigurationId().GetId() + "." + r.GetHref() + appliedConfResources[id] = r + } + } + return appliedConfs, appliedConfResources +} + +// wait for applied configurations to get into DONE or TIMEOUT state +func WaitForAppliedConfigurations(ctx context.Context, t *testing.T, snippetClient pb.SnippetServiceClient, req *pb.GetAppliedConfigurationsRequest, statusFilter map[string][]pb.AppliedConfiguration_Resource_Status) map[string]*pb.AppliedConfiguration_Resource { + var appliedConfResources map[string]*pb.AppliedConfiguration_Resource + retryCount := 0 + for retryCount < 10 { + _, aConfsResources := GetAppliedConfigurations(ctx, t, snippetClient, req) + + for _, r := range aConfsResources { + sf, ok := statusFilter[r.GetHref()] + if !ok { + continue + } + if !slices.Contains(sf, r.GetStatus()) { + goto next + } + } + appliedConfResources = aConfsResources + break + + next: + time.Sleep(time.Millisecond * 200) // 2secs total, enough for PendingCommandsCheckInterval to fire multiple times + retryCount++ + } + return appliedConfResources +} diff --git a/snippet-service/test/condition.go b/snippet-service/test/condition.go new file mode 100644 index 000000000..241cbd8bd --- /dev/null +++ b/snippet-service/test/condition.go @@ -0,0 +1,150 @@ +package test + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/google/uuid" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/pkg/strings" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/stretchr/testify/require" +) + +func ConditionID(i int) string { + if id, ok := RuntimeConfig.conditionIds[i]; ok { + return id + } + id := uuid.NewString() + RuntimeConfig.conditionIds[i] = id + return id +} + +func ConditionName(i int) string { + return "cond" + strconv.Itoa(i) +} + +func stringSlice(prefix string, start, n int) []string { + slice := make([]string, n) + for i := start; i < start+n; i++ { + slice[i-start] = prefix + strconv.Itoa(i) + } + return slice +} + +func ConditionDeviceIdFilter(start, n int) []string { + return strings.Unique(stringSlice("device", start, n)) +} + +func ConditionResourceTypeFilter(start, n int) []string { + return strings.Unique(stringSlice("rt", start, n)) +} + +func ConditionResourceHrefFilter(start, n int) []string { + return strings.Unique(stringSlice("/href/", start, n)) +} + +func ConditionJqExpressionFilter(i int) string { + return "jq" + strconv.Itoa(i) +} + +func ConditionApiAccessToken(i int) string { + return "token" + strconv.Itoa(i) +} + +func getConditions(n int, calcVersion calculateInitialVersionNumber) map[string]store.Condition { + versions := make(map[int]uint64, RuntimeConfig.NumConditions) + owners := make(map[int]string, RuntimeConfig.NumConditions) + conditions := make(map[string]store.Condition) + for i := 0; i < n; i++ { + version, ok := versions[i%RuntimeConfig.NumConditions] + if !ok { + version = 0 + if calcVersion != nil { + version = calcVersion(i) + } + versions[i%RuntimeConfig.NumConditions] = version + } + versions[i%RuntimeConfig.NumConditions]++ + owner, ok := owners[i%RuntimeConfig.NumConditions] + if !ok { + owner = Owner(i % RuntimeConfig.NumOwners) + owners[i%RuntimeConfig.NumConditions] = owner + } + cond := &pb.Condition{ + Id: ConditionID(i % RuntimeConfig.NumConditions), + ConfigurationId: ConfigurationID(i % RuntimeConfig.NumConfigurations), + Enabled: i%2 == 0, + Version: version, + Owner: owner, + DeviceIdFilter: ConditionDeviceIdFilter(i%RuntimeConfig.numDevices, RuntimeConfig.numDevices), + ResourceTypeFilter: ConditionResourceTypeFilter(i%RuntimeConfig.numResourceTypes, RuntimeConfig.numResourceTypes), + ResourceHrefFilter: ConditionResourceHrefFilter(i%RuntimeConfig.numResources, RuntimeConfig.numResources), + JqExpressionFilter: ConditionJqExpressionFilter(i), + ApiAccessToken: ConditionApiAccessToken(i % RuntimeConfig.NumConditions), + Timestamp: time.Now().UnixNano(), + } + cond.Normalize() + condition, ok := conditions[cond.GetId()] + if !ok { + cond.Name = ConditionName(i % RuntimeConfig.NumConditions) + condition = store.MakeFirstCondition(cond) + conditions[cond.GetId()] = condition + continue + } + + cond.Name = condition.Latest.Name + latest := store.ConditionVersion{ + Name: cond.GetName(), + Version: cond.GetVersion(), + Enabled: cond.GetEnabled(), + Timestamp: cond.GetTimestamp(), + DeviceIdFilter: cond.GetDeviceIdFilter(), + ResourceTypeFilter: cond.GetResourceTypeFilter(), + ResourceHrefFilter: cond.GetResourceHrefFilter(), + JqExpressionFilter: cond.GetJqExpressionFilter(), + ApiAccessToken: cond.GetApiAccessToken(), + } + condition.Latest = &latest + condition.Versions = append(condition.Versions, latest) + conditions[cond.GetId()] = condition + } + return conditions +} + +func AddConditionsToStore(ctx context.Context, t *testing.T, s store.Store, n int, calcVersion calculateInitialVersionNumber) map[string]store.Condition { + conditions := getConditions(n, calcVersion) + conditionsToInsert := make([]*store.Condition, 0, len(conditions)) + for _, condition := range conditions { + conditionToInsert := &condition + conditionsToInsert = append(conditionsToInsert, conditionToInsert) + } + err := s.InsertConditions(ctx, conditionsToInsert...) + require.NoError(t, err) + return conditions +} + +func AddConditions(ctx context.Context, t *testing.T, ownerClaim string, ssc pb.SnippetServiceClient, n int, calcVersion calculateInitialVersionNumber) map[string]store.Condition { + conditions := getConditions(n, calcVersion) + for _, c := range conditions { + ctxWithToken := pkgGrpc.CtxWithToken(ctx, GetTokenWithOwnerClaim(t, c.Owner, ownerClaim)) + c.RangeVersions(func(i int, cond *pb.Condition) bool { + if i == 0 { + createdCond, err := ssc.CreateCondition(ctxWithToken, cond) + require.NoError(t, err) + c.Latest.Timestamp = createdCond.GetTimestamp() + c.Versions[i].Timestamp = createdCond.GetTimestamp() + return true + } + updatedCond, err := ssc.UpdateCondition(ctxWithToken, cond) + require.NoError(t, err) + c.Versions[i].Timestamp = updatedCond.GetTimestamp() + return true + }) + conditions[c.Id] = c + } + return conditions +} diff --git a/snippet-service/test/configuration.go b/snippet-service/test/configuration.go new file mode 100644 index 000000000..201af61ab --- /dev/null +++ b/snippet-service/test/configuration.go @@ -0,0 +1,139 @@ +package test + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/go-coap/v3/message" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/stretchr/testify/require" +) + +func Owner(i int) string { + return "owner" + strconv.Itoa(i) +} + +func ConfigurationID(i int) string { + if id, ok := RuntimeConfig.configurationIds[i]; ok { + return id + } + id := uuid.NewString() + RuntimeConfig.configurationIds[i] = id + return id +} + +func ConfigurationName(i int) string { + return "cfg" + strconv.Itoa(i) +} + +func ConfigurationResources(t *testing.T, start, n int) []*pb.Configuration_Resource { + resources := make([]*pb.Configuration_Resource, 0, n) + for i := start; i < start+n; i++ { + resources = append(resources, &pb.Configuration_Resource{ + Href: hubTest.TestResourceLightInstanceHref(strconv.Itoa(i)), + Content: &commands.Content{ + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "power": i, + }), + ContentType: message.AppOcfCbor.String(), + CoapContentFormat: int32(message.AppOcfCbor), + }, + TimeToLive: 1337 + int64(i), + }) + } + return resources +} + +type ( + calculateInitialVersionNumber = func(iteration int) uint64 +) + +func getConfigurations(t *testing.T, n int, calcVersion calculateInitialVersionNumber) map[string]store.Configuration { + versions := make(map[int]uint64, RuntimeConfig.NumConfigurations) + owners := make(map[int]string, RuntimeConfig.NumConfigurations) + configurations := make(map[string]store.Configuration) + for i := 0; i < n; i++ { + version, ok := versions[i%RuntimeConfig.NumConfigurations] + if !ok { + version = 0 + if calcVersion != nil { + version = calcVersion(i) + } + versions[i%RuntimeConfig.NumConfigurations] = version + } + versions[i%RuntimeConfig.NumConfigurations]++ + owner, ok := owners[i%RuntimeConfig.NumConfigurations] + if !ok { + owner = Owner(i % RuntimeConfig.NumOwners) + owners[i%RuntimeConfig.NumConfigurations] = owner + } + conf := &pb.Configuration{ + Id: ConfigurationID(i % RuntimeConfig.NumConfigurations), + Version: version, + Resources: ConfigurationResources(t, i%16, (i%5)+1), + Owner: owner, + Timestamp: time.Now().UnixNano(), + } + conf.Normalize() + configuration, ok := configurations[conf.GetId()] + if !ok { + conf.Name = ConfigurationName(i % RuntimeConfig.NumConfigurations) + configuration = store.MakeFirstConfiguration(conf) + configurations[conf.GetId()] = configuration + continue + } + + conf.Name = configuration.Latest.Name + latest := store.ConfigurationVersion{ + Name: conf.GetName(), + Version: conf.GetVersion(), + Resources: conf.GetResources(), + Timestamp: conf.GetTimestamp(), + } + configuration.Latest = &latest + configuration.Versions = append(configuration.Versions, latest) + configurations[conf.GetId()] = configuration + } + return configurations +} + +func AddConfigurationsToStore(ctx context.Context, t *testing.T, s store.Store, n int, calcVersion calculateInitialVersionNumber) map[string]store.Configuration { + configurations := getConfigurations(t, n, calcVersion) + configurationsToInsert := make([]*store.Configuration, 0, len(configurations)) + for _, c := range configurations { + configurationToInsert := &c + configurationsToInsert = append(configurationsToInsert, configurationToInsert) + } + err := s.InsertConfigurations(ctx, configurationsToInsert...) + require.NoError(t, err) + return configurations +} + +func AddConfigurations(ctx context.Context, t *testing.T, ownerClaim string, ssc pb.SnippetServiceClient, n int, calcVersion calculateInitialVersionNumber) map[string]store.Configuration { + configurations := getConfigurations(t, n, calcVersion) + for _, c := range configurations { + ctxWithToken := pkgGrpc.CtxWithToken(ctx, GetTokenWithOwnerClaim(t, c.Owner, ownerClaim)) + c.RangeVersions(func(i int, conf *pb.Configuration) bool { + if i == 0 { + createdConf, err := ssc.CreateConfiguration(ctxWithToken, conf) + require.NoError(t, err) + c.Latest.Timestamp = createdConf.GetTimestamp() + c.Versions[i].Timestamp = createdConf.GetTimestamp() + return true + } + updatedConf, err := ssc.UpdateConfiguration(ctxWithToken, conf) + require.NoError(t, err) + c.Versions[i].Timestamp = updatedConf.GetTimestamp() + return true + }) + configurations[c.Id] = c + } + return configurations +} diff --git a/snippet-service/test/service.go b/snippet-service/test/service.go new file mode 100644 index 000000000..f8dd8ead1 --- /dev/null +++ b/snippet-service/test/service.go @@ -0,0 +1,158 @@ +package test + +import ( + "context" + "sync" + "time" + + "github.com/plgd-dev/hub/v2/pkg/config/database" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/mongodb" + "github.com/plgd-dev/hub/v2/snippet-service/service" + "github.com/plgd-dev/hub/v2/snippet-service/store" + storeConfig "github.com/plgd-dev/hub/v2/snippet-service/store/config" + storeCqlDB "github.com/plgd-dev/hub/v2/snippet-service/store/cqldb" + storeMongo "github.com/plgd-dev/hub/v2/snippet-service/store/mongodb" + "github.com/plgd-dev/hub/v2/snippet-service/updater" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" +) + +func HTTPURI(uri string) string { + return httpTest.HTTPS_SCHEME + config.SNIPPET_SERVICE_HTTP_HOST + uri +} + +func MakeHTTPConfig() service.HTTPConfig { + return service.HTTPConfig{ + Addr: config.SNIPPET_SERVICE_HTTP_HOST, + Server: config.MakeHttpServerConfig(), + } +} + +func MakeAPIsConfig() service.APIsConfig { + grpc := config.MakeGrpcServerConfig(config.SNIPPET_SERVICE_HOST) + grpc.TLS.ClientCertificateRequired = false + return service.APIsConfig{ + GRPC: grpc, + HTTP: MakeHTTPConfig(), + } +} + +func MakeResourceUpdaterConfig() updater.ResourceUpdaterConfig { + return updater.ResourceUpdaterConfig{ + Connection: config.MakeGrpcClientConfig(config.RESOURCE_AGGREGATE_HOST), + CleanUpExpiredUpdates: "0 * * * *", + ExtendCronParserBySeconds: false, + } +} + +func MakeClientsConfig() service.ClientsConfig { + return service.ClientsConfig{ + Storage: MakeStoreConfig(), + OpenTelemetryCollector: config.MakeOpenTelemetryCollectorClient(), + EventBus: service.EventBusConfig{ + NATS: config.MakeSubscriberConfig(), + SubscriptionID: "snippet-service", + }, + ResourceUpdater: MakeResourceUpdaterConfig(), + } +} + +func MakeStoreConfig() storeConfig.Config { + return storeConfig.Config{ + // TODO: add cqldb support + // Use: config.ACTIVE_DATABASE(), + Use: database.MongoDB, + MongoDB: &storeMongo.Config{ + Mongo: mongodb.Config{ + MaxPoolSize: 16, + MaxConnIdleTime: time.Minute * 4, + URI: config.MONGODB_URI, + Database: "snippetService", + TLS: config.MakeTLSClientConfig(), + }, + }, + CqlDB: &storeCqlDB.Config{ + Embedded: config.MakeCqlDBConfig(), + Table: "snippets", + }, + } +} + +func MakeConfig(t require.TestingT) service.Config { + var cfg service.Config + + cfg.HubID = config.HubID() + cfg.Log = config.MakeLogConfig(t, "TEST_SNIPPET_SERVICE_LOG_LEVEL", "TEST_SNIPPET_SERVICE_LOG_DUMP_BODY") + + cfg.APIs = MakeAPIsConfig() + cfg.Clients = MakeClientsConfig() + + err := cfg.Validate() + require.NoError(t, err) + + return cfg +} + +func SetUp(t require.TestingT) (*service.Service, func()) { + return New(t, MakeConfig(t)) +} + +func New(t require.TestingT, cfg service.Config) (*service.Service, func()) { + ctx := context.Background() + logger := log.NewLogger(cfg.Log) + + fileWatcher, err := fsnotify.NewWatcher(logger) + require.NoError(t, err) + + s, err := service.New(ctx, cfg, fileWatcher, logger) + require.NoError(t, err) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + _ = s.Serve() + }() + + return s, func() { + _ = s.Close() + wg.Wait() + err = fileWatcher.Close() + require.NoError(t, err) + } +} + +func NewStore(t require.TestingT) (store.Store, func()) { + cfg := MakeConfig(t) + if cfg.Clients.Storage.Use == database.CqlDB { + return nil, nil + } + return NewMongoStore(t) +} + +func NewMongoStore(t require.TestingT) (*storeMongo.Store, func()) { + cfg := MakeConfig(t) + logger := log.NewLogger(cfg.Log) + + fileWatcher, err := fsnotify.NewWatcher(logger) + require.NoError(t, err) + + ctx := context.Background() + store, err := storeMongo.New(ctx, cfg.Clients.Storage.MongoDB, fileWatcher, logger, noop.NewTracerProvider()) + require.NoError(t, err) + + cleanUp := func() { + err := store.Clear(ctx) + require.NoError(t, err) + _ = store.Close(ctx) + + err = fileWatcher.Close() + require.NoError(t, err) + } + + return store, cleanUp +} diff --git a/snippet-service/test/test.go b/snippet-service/test/test.go new file mode 100644 index 000000000..7b339b023 --- /dev/null +++ b/snippet-service/test/test.go @@ -0,0 +1,297 @@ +package test + +import ( + "cmp" + "errors" + "slices" + "strings" + "testing" + + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + pbTest "github.com/plgd-dev/hub/v2/test/pb" + "github.com/plgd-dev/kit/v2/codec/json" + "github.com/stretchr/testify/require" +) + +var ( + RuntimeConfig struct { + NumOwners int + numDevices int + numResources int + numResourceTypes int + NumConfigurations int + NumConditions int + configurationIds map[int]string + appliedConfigurationIds map[int]string + conditionIds map[int]string + } + + tokens = make(map[string]string) +) + +func init() { + RuntimeConfig.configurationIds = make(map[int]string) + RuntimeConfig.appliedConfigurationIds = make(map[int]string) + RuntimeConfig.conditionIds = make(map[int]string) + RuntimeConfig.NumOwners = 3 + RuntimeConfig.numDevices = 5 + RuntimeConfig.numResources = 5 + RuntimeConfig.numResourceTypes = 7 + RuntimeConfig.NumConfigurations = 10 + RuntimeConfig.NumConditions = RuntimeConfig.NumConfigurations * 2 +} + +func GetTokenWithOwnerClaim(t *testing.T, owner, ownerClaim string) string { + token, ok := tokens[owner] + if ok { + return token + } + token = oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + ownerClaim: owner, + }) + tokens[owner] = token + return token +} + +func CmpJSON(t *testing.T, want, got interface{}) { + wantJson, err := json.Encode(want) + require.NoError(t, err) + gotJson, err := json.Encode(got) + require.NoError(t, err) + require.JSONEq(t, string(wantJson), string(gotJson)) +} + +func cmpConfigurationResources(t *testing.T, want, got []*pb.Configuration_Resource) { + require.Len(t, got, len(want)) + for i := range want { + wantData, ok := test.DecodeCbor(t, want[i].GetContent().GetData()).(map[interface{}]interface{}) + require.True(t, ok) + gotData, ok := test.DecodeCbor(t, got[i].GetContent().GetData()).(map[interface{}]interface{}) + require.True(t, ok) + require.Equal(t, wantData, gotData) + want[i].Content.Data = nil + got[i].Content.Data = nil + } + CmpJSON(t, want, got) +} + +func MergeConfigurations(c1, c2 *store.Configuration) error { + if c1.Id != c2.Id || c1.Owner != c2.Owner { + return errors.New("conditions to merge must have the same ID and owner") + } + + if c2.Latest != nil { + latest := c2.Latest.Copy() + c1.Latest = &latest + } + c1.Versions = append(c1.Versions, c2.Versions...) + slices.SortFunc(c1.Versions, func(i, j store.ConfigurationVersion) int { + return cmp.Compare(i.Version, j.Version) + }) + c1.Versions = slices.CompactFunc(c1.Versions, func(i, j store.ConfigurationVersion) bool { + return i.Version == j.Version + }) + + if c1.Latest != nil { + if !slices.ContainsFunc(c1.Versions, func(cv store.ConfigurationVersion) bool { + return cv.Version == c1.Latest.Version + }) { + c1.Versions = append(c1.Versions, c1.Latest.Copy()) + } + } else if len(c1.Versions) > 0 { + latest := c1.Versions[len(c1.Versions)-1].Copy() + c1.Latest = &latest + } + return nil +} + +func CmpConfiguration(t *testing.T, want, got *pb.Configuration, ignoreTimestamp bool) { + want = want.Clone() + got = got.Clone() + if ignoreTimestamp { + want.Timestamp = got.GetTimestamp() + } + if want.GetResources() != nil && got.GetResources() != nil { + cmpConfigurationResources(t, want.GetResources(), got.GetResources()) + want.Resources = nil + got.Resources = nil + } + CmpJSON(t, want, got) +} + +func ConfigurationContains(t *testing.T, storeConf store.Configuration, conf *pb.Configuration) { + require.Equal(t, storeConf.Id, conf.GetId()) + require.Equal(t, storeConf.Owner, conf.GetOwner()) + for _, v := range storeConf.Versions { + if v.Version != conf.GetVersion() { + continue + } + test.CheckProtobufs(t, v.Resources, conf.GetResources(), test.RequireToCheckFunc(require.Equal)) + return + } + require.Fail(t, "version not found") +} + +func CmpStoredConfiguration(t *testing.T, want, got *store.Configuration, ignoreTimestamp, ignoreLatest bool) { + require.Len(t, got.Versions, len(want.Versions)) + if ignoreTimestamp || ignoreLatest { + want = want.Clone() + got = got.Clone() + } + if ignoreTimestamp { + if want.Latest != nil && got.Latest != nil { + want.Latest.Timestamp = got.Latest.Timestamp + } + for i := range want.Versions { + want.Versions[i].Timestamp = got.Versions[i].Timestamp + } + } + if ignoreLatest { + want.Latest = got.Latest + } + CmpJSON(t, want, got) +} + +func CmpStoredConfigurationMaps(t *testing.T, want, got map[string]store.Configuration) { + require.Len(t, got, len(want)) + for _, v := range want { + gotV, ok := got[v.Id] + require.True(t, ok) + CmpStoredConfiguration(t, &v, &gotV, true, false) + } +} + +func MergeConditions(c1, c2 *store.Condition) error { + if c1.Id != c2.Id || c1.Owner != c2.Owner || c1.ConfigurationId != c2.ConfigurationId { + return errors.New("conditions to merge must have the same ID, owner and configuration ID") + } + + if c2.Latest != nil { + latest := c2.Latest.Copy() + c1.Latest = &latest + } + c1.Versions = append(c1.Versions, c2.Versions...) + slices.SortFunc(c1.Versions, func(i, j store.ConditionVersion) int { + return cmp.Compare(i.Version, j.Version) + }) + c1.Versions = slices.CompactFunc(c1.Versions, func(i, j store.ConditionVersion) bool { + return i.Version == j.Version + }) + + if c1.Latest != nil { + if !slices.ContainsFunc(c1.Versions, func(cv store.ConditionVersion) bool { + return cv.Version == c1.Latest.Version + }) { + c1.Versions = append(c1.Versions, c1.Latest.Copy()) + } + } else if len(c1.Versions) > 0 { + latest := c1.Versions[len(c1.Versions)-1].Copy() + c1.Latest = &latest + } + return nil +} + +func CmpCondition(t *testing.T, want, got *pb.Condition, ignoreTimestamp bool) { + want = want.Clone() + if ignoreTimestamp { + want.Timestamp = got.GetTimestamp() + } + CmpJSON(t, want, got) +} + +func ConditionContains(t *testing.T, storeCond store.Condition, cond *pb.Condition) { + require.Equal(t, storeCond.Id, cond.GetId()) + require.Equal(t, storeCond.Owner, cond.GetOwner()) + require.Equal(t, storeCond.ConfigurationId, cond.GetConfigurationId()) + for _, v := range storeCond.Versions { + if v.Version != cond.GetVersion() { + continue + } + require.Equal(t, v.Name, cond.GetName()) + require.Equal(t, v.Version, cond.GetVersion()) + require.Equal(t, v.Enabled, cond.GetEnabled()) + require.Equal(t, v.Timestamp, cond.GetTimestamp()) + require.Equal(t, v.DeviceIdFilter, cond.GetDeviceIdFilter()) + require.Equal(t, v.ResourceTypeFilter, cond.GetResourceTypeFilter()) + require.Equal(t, v.ResourceHrefFilter, cond.GetResourceHrefFilter()) + require.Equal(t, v.JqExpressionFilter, cond.GetJqExpressionFilter()) + require.Equal(t, v.ApiAccessToken, cond.GetApiAccessToken()) + return + } + require.Fail(t, "version not found") +} + +func CmpStoredCondition(t *testing.T, want, got *store.Condition, ignoreTimestamp, ignoreLatest bool) { + require.Len(t, got.Versions, len(want.Versions)) + if ignoreTimestamp || ignoreLatest { + want = want.Clone() + got = got.Clone() + } + if ignoreTimestamp { + if want.Latest != nil && got.Latest != nil { + want.Latest.Timestamp = got.Latest.Timestamp + } + for i := range want.Versions { + want.Versions[i].Timestamp = got.Versions[i].Timestamp + } + } + if ignoreLatest { + want.Latest = got.Latest + } + CmpJSON(t, want, got) +} + +func CmpStoredConditionMaps(t *testing.T, want, got map[string]store.Condition) { + require.Len(t, got, len(want)) + for _, v := range want { + gotV, ok := got[v.Id] + require.True(t, ok) + CmpStoredCondition(t, &v, &gotV, false, false) + } +} + +func CmpAppliedDeviceConfiguration(t *testing.T, want, got *pb.AppliedConfiguration, ignoreTimestamp bool) { + want = want.Clone() + got = got.Clone() + if ignoreTimestamp { + want.Timestamp = got.GetTimestamp() + } + require.Len(t, got.GetResources(), len(want.GetResources())) + + normalizeResources := func(resources []*pb.AppliedConfiguration_Resource) []*pb.AppliedConfiguration_Resource { + resources = slices.CompactFunc(resources, func(i, j *pb.AppliedConfiguration_Resource) bool { + return i.GetHref() == j.GetHref() + }) + slices.SortFunc(resources, func(i, j *pb.AppliedConfiguration_Resource) int { + return strings.Compare(i.GetHref(), j.GetHref()) + }) + return resources + } + want.Resources = normalizeResources(want.GetResources()) + got.Resources = normalizeResources(got.GetResources()) + + for i := range want.GetResources() { + wantResource := want.GetResources()[i] + gotResource := got.GetResources()[i] + if wantResource.GetResourceUpdated() != nil && gotResource.GetResourceUpdated() != nil { + pbTest.CmpResourceUpdated(t, wantResource.GetResourceUpdated(), gotResource.GetResourceUpdated()) + wantResource.ResourceUpdated = nil + gotResource.ResourceUpdated = nil + } + } + + CmpJSON(t, want, got) +} + +func CmpAppliedDeviceConfigurationsMaps(t *testing.T, want, got map[string]*pb.AppliedConfiguration, ignoreTimestamp bool) { + require.Len(t, got, len(want)) + for _, v := range want { + gotV, ok := got[v.GetId()] + require.True(t, ok) + CmpAppliedDeviceConfiguration(t, v, gotV, ignoreTimestamp) + } +} diff --git a/snippet-service/updater/config.go b/snippet-service/updater/config.go new file mode 100644 index 000000000..cd645b8d2 --- /dev/null +++ b/snippet-service/updater/config.go @@ -0,0 +1,42 @@ +package updater + +import ( + "fmt" + "time" + + "github.com/go-co-op/gocron/v2" + "github.com/plgd-dev/hub/v2/pkg/log" + grpcClient "github.com/plgd-dev/hub/v2/pkg/net/grpc/client" +) + +type ResourceUpdaterConfig struct { + Connection grpcClient.Config `yaml:"grpc" json:"grpc"` + CleanUpExpiredUpdates string `yaml:"cleanUpExpiredUpdates" json:"cleanUpExpiredUpdates"` + ExtendCronParserBySeconds bool `yaml:"-" json:"-"` +} + +func (c *ResourceUpdaterConfig) Validate() error { + if err := c.Connection.Validate(); err != nil { + return fmt.Errorf("grpc.%w", err) + } + if c.CleanUpExpiredUpdates == "" { + return nil + } + s, err := gocron.NewScheduler(gocron.WithLocation(time.Local)) //nolint:gosmopolitan + if err != nil { + return fmt.Errorf("cannot create cron job: %w", err) + } + defer func() { + if errS := s.Shutdown(); errS != nil { + log.Errorf("failed to shutdown cron job: %w", errS) + } + }() + _, err = s.NewJob(gocron.CronJob(c.CleanUpExpiredUpdates, c.ExtendCronParserBySeconds), + gocron.NewTask(func() { + // do nothing + })) + if err != nil { + return fmt.Errorf("cleanUpExpiredUpdates('%v') - %w", c.CleanUpExpiredUpdates, err) + } + return nil +} diff --git a/snippet-service/updater/config_test.go b/snippet-service/updater/config_test.go new file mode 100644 index 000000000..5647851e0 --- /dev/null +++ b/snippet-service/updater/config_test.go @@ -0,0 +1,58 @@ +package updater_test + +import ( + "testing" + + "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/snippet-service/updater" + "github.com/stretchr/testify/require" +) + +func TestResourceAggregateConfig(t *testing.T) { + tests := []struct { + name string + cfg updater.ResourceUpdaterConfig + wantErr bool + }{ + { + name: "valid", + cfg: test.MakeResourceUpdaterConfig(), + }, + { + name: "valid - no cron", + cfg: func() updater.ResourceUpdaterConfig { + cfg := test.MakeResourceUpdaterConfig() + cfg.CleanUpExpiredUpdates = "" + return cfg + }(), + }, + { + name: "invalid - no connection", + cfg: func() updater.ResourceUpdaterConfig { + cfg := updater.ResourceUpdaterConfig{} + return cfg + }(), + wantErr: true, + }, + { + name: "invalid - bad cron expression", + cfg: func() updater.ResourceUpdaterConfig { + cfg := test.MakeResourceUpdaterConfig() + cfg.CleanUpExpiredUpdates = "bad" + return cfg + }(), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/snippet-service/updater/expiredUpdates.go b/snippet-service/updater/expiredUpdates.go new file mode 100644 index 000000000..a50c74b1f --- /dev/null +++ b/snippet-service/updater/expiredUpdates.go @@ -0,0 +1,23 @@ +package updater + +import ( + "fmt" + "time" + + "github.com/go-co-op/gocron/v2" +) + +func NewExpiredUpdatesChecker(cleanUpExpiredUpdates string, withSeconds bool, updater *ResourceUpdater) (gocron.Scheduler, error) { + s, err := gocron.NewScheduler(gocron.WithLocation(time.Local)) //nolint:gosmopolitan + if err != nil { + return nil, fmt.Errorf("cannot create cron job: %w", err) + } + _, err = s.NewJob(gocron.CronJob(cleanUpExpiredUpdates, withSeconds), gocron.NewTask(func() { + updater.TimeoutPendingResourceUpdates() + })) + if err != nil { + return nil, fmt.Errorf("cannot create cron job: %w", err) + } + s.Start() + return s, nil +} diff --git a/snippet-service/updater/resourceUpdater.go b/snippet-service/updater/resourceUpdater.go new file mode 100644 index 000000000..9741a194b --- /dev/null +++ b/snippet-service/updater/resourceUpdater.go @@ -0,0 +1,729 @@ +package updater + +import ( + "cmp" + "context" + "errors" + "fmt" + "slices" + "strings" + "time" + + "github.com/go-co-op/gocron/v2" + "github.com/google/uuid" + "github.com/hashicorp/go-multierror" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + grpcClient "github.com/plgd-dev/hub/v2/pkg/net/grpc/client" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus" + "github.com/plgd-dev/hub/v2/resource-aggregate/events" + raService "github.com/plgd-dev/hub/v2/resource-aggregate/service" + "github.com/plgd-dev/hub/v2/snippet-service/jq" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/codes" +) + +type ResourceUpdater struct { + ctx context.Context + storage store.Store + raConn *grpcClient.Client + raClient raService.ResourceAggregateClient + scheduler gocron.Scheduler + logger log.Logger +} + +func NewResourceUpdater(ctx context.Context, config ResourceUpdaterConfig, storage store.Store, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*ResourceUpdater, error) { + raConn, err := grpcClient.New(config.Connection, fileWatcher, logger, tracerProvider) + if err != nil { + return nil, fmt.Errorf("cannot connect to resource aggregate: %w", err) + } + + ru := &ResourceUpdater{ + ctx: ctx, + storage: storage, + raConn: raConn, + raClient: raService.NewResourceAggregateClient(raConn.GRPC()), + logger: logger, + } + if config.CleanUpExpiredUpdates != "" { + scheduler, err := NewExpiredUpdatesChecker(config.CleanUpExpiredUpdates, config.ExtendCronParserBySeconds, ru) + if err != nil { + return nil, fmt.Errorf("cannot create scheduler: %w", err) + } + ru.scheduler = scheduler + } + return ru, nil +} + +type evaluateCondition = func(condition *pb.Condition) bool + +func (h *ResourceUpdater) getConditions(ctx context.Context, owner, deviceID, resourceHref string, resourceTypes []string, eval evaluateCondition) ([]*pb.Condition, error) { + conditions := make([]*pb.Condition, 0, 4) + err := h.storage.GetLatestEnabledConditions(ctx, owner, &store.GetLatestConditionsQuery{ + DeviceID: deviceID, + ResourceHref: resourceHref, + ResourceTypeFilter: resourceTypes, + }, func(v *store.Condition) error { + c, errG := v.GetLatest() + if errG != nil { + return fmt.Errorf("cannot get condition: %w", errG) + } + if !eval(c) { + h.logger.Debugf("condition(%v) skipped", c) + return nil + } + conditions = append(conditions, c.Clone()) + return nil + }) + if err != nil { + return nil, fmt.Errorf("cannot get latest conditions: %w", err) + } + return conditions, nil +} + +func (h *ResourceUpdater) applyExecution(ctx context.Context, execution execution, resourceID *commands.ResourceId, configurationID, correlationID string, cr *pb.Configuration_Resource) executionResult { + if execution.executeBy == executeByTypeOnDemand { + validUntil, err := h.applyConfigurationToResource(ctx, resourceID, configurationID, correlationID, cr, execution.onDemand.token) + if err != nil { + return executionResult{err: err} + } + return executionResult{ + validUntil: validUntil, + onDemand: execution.onDemand, + executedBy: executeByTypeOnDemand, + } + } + + if execution.executeBy == executeByTypeCondition { + validUntil, err := h.applyConfigurationToResource(ctx, resourceID, configurationID, correlationID, cr, execution.condition.token) + if err != nil { + return executionResult{err: err} + } + return executionResult{ + validUntil: validUntil, + condition: execution.condition, + executedBy: executeByTypeCondition, + } + } + + validUntil, appliedCond, err := h.findTokenAndApplyConfigurationToResource(ctx, resourceID, configurationID, correlationID, cr, execution.conditions) + if err != nil { + return executionResult{err: err} + } + return executionResult{ + validUntil: validUntil, + condition: appliedCond, + executedBy: executeByTypeCondition, + } +} + +type configurationWithExecution struct { + configuration *pb.Configuration + execution execution +} + +func (h *ResourceUpdater) getConfigurationsByConditions(ctx context.Context, owner string, conditions []*pb.Condition) ([]configurationWithExecution, error) { + confsToConditions := make(map[string][]*pb.Condition) + idFilter := make([]*pb.IDFilter, 0, len(conditions)) + for _, c := range conditions { + confID := c.GetConfigurationId() + if confID == "" { + h.logger.Warnf("invalid condition(%v)", c) + continue + } + if c.GetApiAccessToken() == "" { + h.logger.Warnf("skipping condition(%v) with no token", c) + continue + } + confConditions := confsToConditions[confID] + confConditions = append(confConditions, c.Clone()) + confsToConditions[confID] = confConditions + idFilter = append(idFilter, &pb.IDFilter{ + Id: confID, + Version: &pb.IDFilter_Latest{ + Latest: true, + }, + }) + } + if (len(idFilter)) == 0 { + return []configurationWithExecution{}, nil + } + + h.logger.Debugf("getting configurations for conditions: %v", idFilter) + + // get configurations + configurations := make([]*pb.Configuration, 0, 4) + err := h.storage.GetConfigurations(ctx, owner, &pb.GetConfigurationsRequest{ + IdFilter: idFilter, + }, func(v *store.Configuration) error { + c, errG := v.GetLatest() + if errG != nil { + return fmt.Errorf("cannot get configuration: %w", errG) + } + configurations = append(configurations, c.Clone()) + return nil + }) + if err != nil { + return nil, fmt.Errorf("cannot get configurations: %w", err) + } + + ids := make([]string, 0, len(configurations)) + for _, c := range configurations { + ids = append(ids, c.GetId()) + } + h.logger.Debugf("got configurations for conditions: %v", ids) + + confsWithConditions := make([]configurationWithExecution, 0, len(configurations)) + for _, c := range configurations { + confConditions := confsToConditions[c.GetId()] + if len(confConditions) == 0 { + h.logger.Errorf("no conditions found for configuration(id:%v)", c.GetId()) + continue + } + slices.SortFunc(confConditions, func(i, j *pb.Condition) int { + return cmp.Compare(i.GetApiAccessToken(), j.GetApiAccessToken()) + }) + confConditions = slices.CompactFunc(confConditions, func(i, j *pb.Condition) bool { + return i.GetApiAccessToken() == j.GetApiAccessToken() + }) + confsWithConditions = append(confsWithConditions, configurationWithExecution{ + configuration: c.Clone(), + execution: execution{ + conditions: confConditions, + executeBy: executeByTypeFindCondition, + }, + }) + condIDs := make([]string, 0, len(confConditions)) + for _, cond := range confConditions { + condIDs = append(condIDs, cond.GetId()) + } + h.logger.Debugf("found %v conditions for configuration(id:%v)", condIDs, c.GetId()) + } + + return confsWithConditions, nil +} + +func (h *ResourceUpdater) getConfigurations(ctx context.Context, owner, deviceID, resourceHref string, resourceTypes []string, eval evaluateCondition) ([]configurationWithExecution, error) { + // get matching conditions + conditions, err := h.getConditions(ctx, owner, deviceID, resourceHref, resourceTypes, eval) + if err != nil { + return nil, err + } + h.logger.Debugf("found %v conditions for resource changed event(deviceID:%v, href:%v, resourceTypes %v)", len(conditions), deviceID, resourceHref, resourceTypes) + + // get configurations with tokens + return h.getConfigurationsByConditions(ctx, owner, conditions) +} + +func (h *ResourceUpdater) applyConfigurationToResource(ctx context.Context, resourceID *commands.ResourceId, configurationID, correlationID string, cr *pb.Configuration_Resource, token string) (int64, error) { + h.logger.Debugf("applying configuration(id:%v) to resource(%v)", configurationID, resourceID.GetHref()) + upd := &commands.UpdateResourceRequest{ + ResourceId: resourceID, + CorrelationId: correlationID, + Content: cr.GetContent(), + TimeToLive: cr.GetTimeToLive(), + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: configurationID, + }, + Force: true, + } + if token != "" { + ctx = pkgGrpc.CtxWithToken(ctx, token) + } + res, err := h.raClient.UpdateResource(ctx, upd) + if err != nil { + h.logger.Errorf("failed to apply configuration(id:%v) to resource(%v): %w", configurationID, resourceID.GetHref(), err) + return 0, err + } + h.logger.Infof("configuration(id:%v) applied to resource(%v)", configurationID, resourceID.GetHref()) + return res.GetValidUntil(), nil +} + +func (h *ResourceUpdater) findTokenAndApplyConfigurationToResource(ctx context.Context, resourceID *commands.ResourceId, configurationID, correlationID string, cr *pb.Configuration_Resource, conditions []*pb.Condition) (int64, appliedCondition, error) { + for _, cond := range conditions { + condID := cond.GetId() + token := cond.GetApiAccessToken() + validUntil, err := h.applyConfigurationToResource(ctx, resourceID, configurationID, correlationID, cr, token) + if err != nil { + if grpcCode := pkgGrpc.ErrToStatus(err).Code(); grpcCode == codes.Unauthenticated { + h.logger.Debugf("cannot apply configuration(id:%v) to resource(%v): invalid token", configurationID, resourceID.GetHref()) + continue + } + h.logger.Errorf("cannot apply configuration(id:%v) to resource(%v): %w", configurationID, resourceID.GetHref(), err) + return 0, appliedCondition{}, err + } + return validUntil, appliedCondition{id: condID, version: cond.GetVersion(), token: token}, nil + } + return 0, appliedCondition{}, errors.New("cannot apply configuration: no valid token found") +} + +func resourceCorrelationID(ids ...string) string { + cID := "" + for _, id := range ids { + if id == "" { + continue + } + + if cID != "" { + cID += "." + } + cID += id + } + return cID +} + +// 1) "appliedConfigurationID.resourceCorrelationID" if the configuration was applied by a condition +// 2) "appliedConfigurationID.resourceCorrelationID{. InvokeConfiguration correlationID}" if the configuration was applied on demand by InvokeConfiguration +func SplitCorrelationID(correlationID string) (string, string, string, bool) { + parts := strings.Split(correlationID, ".") + if len(parts) < 2 || len(parts) > 3 { + return "", "", "", false + } + appliedConfID := parts[0] + resourceCorrelationID := parts[1] + customCorrelationID := "" + if len(parts) > 2 { + customCorrelationID = parts[2] + } + return appliedConfID, resourceCorrelationID, customCorrelationID, true +} + +func getAppliedConfigurationResources(resources []*pb.Configuration_Resource, appliedConfID, correlationID string) ([]*pb.AppliedConfiguration_Resource, map[string]string) { + updatedResources := make([]*pb.AppliedConfiguration_Resource, 0, len(resources)) + updatedResourceCorIDs := make(map[string]string) + for _, cr := range resources { + hrefCorrelationID := uuid.NewString() + resCorrelationID := resourceCorrelationID(appliedConfID, hrefCorrelationID, correlationID) + updatedResourceCorIDs[cr.GetHref()] = resCorrelationID + updatedResources = append(updatedResources, &pb.AppliedConfiguration_Resource{ + Href: cr.GetHref(), + CorrelationId: resCorrelationID, + Status: pb.AppliedConfiguration_Resource_QUEUED, + }) + } + return updatedResources, updatedResourceCorIDs +} + +func makeResourceUpdatedWithError(confID, owner, correlationID string, resourceID *commands.ResourceId, err error) *events.ResourceUpdated { + return &events.ResourceUpdated{ + ResourceId: resourceID, + Status: commands.Status_ERROR, + Content: &commands.Content{ + Data: []byte(err.Error()), + ContentType: message.TextPlain.String(), + }, + AuditContext: &commands.AuditContext{ + CorrelationId: correlationID, + Owner: owner, + }, + EventMetadata: &events.EventMetadata{ + ConnectionId: confID, + }, + } +} + +func getUpdateAppliedConfigurationResourceRequest(appliedConfID, confID, owner, correlationID string, resourceID *commands.ResourceId, execution executionResult, setExecutionConditionID bool) store.UpdateAppliedConfigurationResourceRequest { + update := store.UpdateAppliedConfigurationResourceRequest{ + AppliedConfigurationID: appliedConfID, + StatusFilter: []pb.AppliedConfiguration_Resource_Status{pb.AppliedConfiguration_Resource_QUEUED}, + Resource: &pb.AppliedConfiguration_Resource{ + Href: resourceID.GetHref(), + CorrelationId: correlationID, + }, + } + if execution.err == nil { + // update resource status from queued to pending + update.Resource.Status = pb.AppliedConfiguration_Resource_PENDING + update.Resource.ValidUntil = execution.validUntil + if setExecutionConditionID { + update.AppliedCondition = &pb.AppliedConfiguration_LinkedTo{ + Id: execution.condition.id, + Version: execution.condition.version, + } + } + return update + } + + update.Resource.Status = pb.AppliedConfiguration_Resource_DONE + update.Resource.ResourceUpdated = makeResourceUpdatedWithError(confID, owner, correlationID, resourceID, execution.err) + return update +} + +func (h *ResourceUpdater) cancelPendingResourceUpdate(ctx context.Context, resourceID *commands.ResourceId, correlationID, configurationID string) error { + cancelReq := &commands.CancelPendingCommandsRequest{ + ResourceId: resourceID, + CorrelationIdFilter: []string{correlationID}, + CommandMetadata: &commands.CommandMetadata{ + ConnectionId: configurationID, + }, + } + _, err := h.raClient.CancelPendingCommands(ctx, cancelReq) + if err != nil { + h.logger.Debugf("failed to cancel pending resource(%v) update: %v", resourceID.ToString(), err) + } else { + h.logger.Debugf("pending resource(%v) update canceled", resourceID.ToString()) + } + return err +} + +func (h *ResourceUpdater) cancelPendingResourceUpdates(appliedConf *pb.AppliedConfiguration, token string) { + h.logger.Debugf("canceling pending resource operations for replaced configuration(%s)", appliedConf.GetId()) + + for _, res := range appliedConf.GetResources() { + if res.GetStatus() != pb.AppliedConfiguration_Resource_PENDING { + continue + } + // since we are using the Force flag in the UpdateResourceRequest, we need to cancel the pending commands, + // otherwise commands for not existing resources will remain in the pending state forever + resourceID := &commands.ResourceId{DeviceId: appliedConf.GetDeviceId(), Href: res.GetHref()} + _ = h.cancelPendingResourceUpdate(pkgGrpc.CtxWithToken(h.ctx, token), resourceID, res.GetCorrelationId(), appliedConf.GetConfigurationId().GetId()) + } +} + +func ctxWithToken(ctx context.Context, token string) context.Context { + if token == "" { + return ctx + } + return pkgGrpc.CtxWithToken(ctx, token) +} + +func (h *ResourceUpdater) applyConfigurationToResources(ctx context.Context, owner, deviceID, correlationID string, confWithExecution *configurationWithExecution) (*pb.AppliedConfiguration, error) { + h.logger.Debugf("applying configuration(id:%v)", confWithExecution.configuration.GetId()) + appliedConfID := uuid.NewString() + resources, resourcesCorIDs := getAppliedConfigurationResources(confWithExecution.configuration.GetResources(), appliedConfID, correlationID) + create := &pb.AppliedConfiguration{ + Id: appliedConfID, + Owner: owner, + DeviceId: deviceID, + ConfigurationId: pb.MakeLinkedTo(confWithExecution.configuration.GetId(), confWithExecution.configuration.GetVersion()), + Resources: resources, + Timestamp: time.Now().UnixNano(), + } + confWithExecution.execution.setExecutedBy(create) + + appliedConf, oldAppliedConf, errC := h.storage.CreateAppliedConfiguration(ctx, create, confWithExecution.execution.force) + if errC != nil { + return nil, fmt.Errorf("cannot create applied device configuration: %w", errC) + } + if oldAppliedConf != nil { + h.cancelPendingResourceUpdates(oldAppliedConf, confWithExecution.execution.token()) + } + h.logger.Debugf("applied configuration created: %v", appliedConf) + + var errs *multierror.Error + for _, cr := range confWithExecution.configuration.GetResources() { + href := cr.GetHref() + resourceID := &commands.ResourceId{Href: href, DeviceId: deviceID} + confID := confWithExecution.configuration.GetId() + resCorrelationID := resourcesCorIDs[href] + exRes := h.applyExecution(ctx, confWithExecution.execution, resourceID, confID, resCorrelationID, cr) + updateExecutionConditionID := false + if exRes.executedBy == executeByTypeCondition { + // update for next iteration + // first resources always iterates conditions and on success it returns the condition id, + // the same condition id is used for the remaining resources + confWithExecution.execution.setCondition(exRes.condition) + if exRes.condition.id != appliedConf.GetConditionId().GetId() { + updateExecutionConditionID = true + } + } + update := getUpdateAppliedConfigurationResourceRequest(appliedConf.GetId(), confID, owner, resCorrelationID, resourceID, exRes, updateExecutionConditionID) + h.logger.Debugf("updating applied configuration(%v) resource(%v) with status(%v)", appliedConf.GetId(), href, update.Resource.GetStatus().String()) + var err error + updatedAppliedConf, err := h.storage.UpdateAppliedConfigurationResource(ctx, owner, update) + if err == nil { + appliedConf = updatedAppliedConf + continue + } + if errors.Is(err, store.ErrNotFound) { // the appliedConfiguration doesnt exists -> it was removed by forced InvokeConfiguration from other thread + _ = h.cancelPendingResourceUpdate(ctxWithToken(ctx, exRes.token()), resourceID, resCorrelationID, confID) + return nil, err + } + // the resource is not in queued status -> it was already updated by other goroutine -> skip + h.logger.Errorf("cannot update applied configuration resource: %w", err) + errs = multierror.Append(errs, err) + } + return appliedConf, errs.ErrorOrNil() +} + +func decodeContent(content *commands.Content) (interface{}, error) { + var rcData map[string]any + err := commands.DecodeContent(content, &rcData) + if err == nil { + return rcData, nil + } + // content could be a single value or an array + var rcData2 interface{} + err = commands.DecodeContent(content, &rcData2) + if err == nil { + return rcData2, nil + } + return nil, fmt.Errorf("cannot decode content: %w", err) +} + +func (h *ResourceUpdater) applyConfigurationsByConditions(ctx context.Context, rc *events.ResourceChanged) error { + owner := rc.GetAuditContext().GetOwner() + if owner == "" { + return errors.New("owner not set") + } + + rcData, err := decodeContent(rc.GetContent()) + if err != nil { + return err + } + + eval := func(condition *pb.Condition) bool { + jqe := condition.GetJqExpressionFilter() + if jqe == "" { + return true + } + ok, errE := jq.EvalJQCondition(jqe, rcData) + if errE != nil { + h.logger.Error(errE) + return false + } + return ok + } + + resourceID := rc.GetResourceId() + deviceID := resourceID.GetDeviceId() + resourceHref := resourceID.GetHref() + resourceTypes := rc.GetResourceTypes() + confsWithConditions, err := h.getConfigurations(ctx, owner, deviceID, resourceHref, resourceTypes, eval) + if err != nil { + return err + } + if len(confsWithConditions) == 0 { + return nil + } + + // apply configurations to resources + var errs *multierror.Error + for _, c := range confsWithConditions { + if len(c.configuration.GetResources()) == 0 { + h.logger.Debugf("no resources found for configuration(id:%v) for device %s", c.configuration.GetId(), deviceID) + continue + } + _, errA := h.applyConfigurationToResources(ctx, owner, deviceID, "", &c) + if store.IsDuplicateKeyError(errA) { + // applied configuration already exists + h.logger.Debugf("applied configuration already exists for device(%s) and configuration(%s): %v", deviceID, + c.configuration.GetId(), errA) + continue + } + if errA != nil { + errs = multierror.Append(errs, errA) + } + } + return errs.ErrorOrNil() +} + +func isValidUUID(s string) bool { + _, err := uuid.Parse(s) + return err == nil +} + +func (h *ResourceUpdater) finishPendingConfiguration(ctx context.Context, updated *events.ResourceUpdated) error { + correlationID := updated.GetAuditContext().GetCorrelationId() + // correlationID from snippet-service is in the form of + appliedConfID, resourcesCorrelationID, _, ok := SplitCorrelationID(correlationID) + if !ok || !isValidUUID(appliedConfID) || !isValidUUID(resourcesCorrelationID) { + return nil + } + h.logger.Debugf("finishing pending configuration(%v) update for resource(%v:%v): %v", appliedConfID, updated.GetResourceId().GetDeviceId(), updated.GetResourceId().GetHref(), updated) + owner := updated.GetAuditContext().GetOwner() + _, err := h.storage.UpdateAppliedConfigurationResource(ctx, owner, store.UpdateAppliedConfigurationResourceRequest{ + AppliedConfigurationID: appliedConfID, + Resource: &pb.AppliedConfiguration_Resource{ + Href: updated.GetResourceId().GetHref(), + CorrelationId: correlationID, + Status: pb.AppliedConfiguration_Resource_DONE, + ResourceUpdated: updated, + }, + }) + if updated.GetStatus() == commands.Status_CANCELED && errors.Is(err, store.ErrNotFound) { + // the pending update was canceled by h.cancelPendingResourceUpdates and the configuration was removed + return nil + } + return err +} + +func (h *ResourceUpdater) handleResourceChanged(ctx context.Context, ev eventbus.EventUnmarshaler) error { + var changed events.ResourceChanged + if err := ev.Unmarshal(&changed); err != nil { + return fmt.Errorf("cannot unmarshal ResourceChanged event: %w", err) + } + if err := h.applyConfigurationsByConditions(ctx, &changed); err != nil { + return fmt.Errorf("cannot apply configurations for event (deviceID: %v, href: %v, resourceTypes: %v): %w", changed.GetResourceId().GetDeviceId(), changed.GetResourceId().GetHref(), changed.GetResourceTypes(), err) + } + return nil +} + +func (h *ResourceUpdater) handleResourceUpdated(ctx context.Context, ev eventbus.EventUnmarshaler) error { + var updated events.ResourceUpdated + if err := ev.Unmarshal(&updated); err != nil { + return fmt.Errorf("cannot unmarshal ResourceUpdated event: %w", err) + } + if err := h.finishPendingConfiguration(ctx, &updated); err != nil && !errors.Is(err, store.ErrNotFound) { + return fmt.Errorf("failed to finish pending applied configuration for resource(%v): %w", updated.GetResourceId().GetHref(), err) + } + return nil +} + +func (h *ResourceUpdater) Handle(ctx context.Context, iter eventbus.Iter) error { + for { + ev, ok := iter.Next(ctx) + if !ok { + return iter.Err() + } + if ev.EventType() == (&events.ResourceChanged{}).EventType() { + if err := h.handleResourceChanged(ctx, ev); err != nil { + h.logger.Errorf("cannot handle resource changed event: %w", err) + } + continue + } + if ev.EventType() == (&events.ResourceUpdated{}).EventType() { + if err := h.handleResourceUpdated(ctx, ev); err != nil { + h.logger.Errorf("cannot handle resource updated event: %w", err) + } + continue + } + + h.logger.Errorf("unexpected event type: %v", ev.EventType()) + } +} + +func (h *ResourceUpdater) applyConfigurationOnDemand(ctx context.Context, conf *pb.Configuration, token, owner, deviceID, correlationID string, force bool) (*pb.AppliedConfiguration, error) { + if len(conf.GetResources()) == 0 { + h.logger.Debugf("no resources found for configuration(id:%v) for device %s", conf.GetId(), deviceID) + return nil, nil + } + + return h.applyConfigurationToResources(ctx, owner, deviceID, correlationID, &configurationWithExecution{ + configuration: conf, + execution: execution{ + executeBy: executeByTypeOnDemand, + onDemand: appliedOnDemand{ + token: token, + }, + force: force, + }, + }) +} + +func (h *ResourceUpdater) InvokeConfiguration(ctx context.Context, token, owner string, req *pb.InvokeConfigurationRequest) (*pb.AppliedConfiguration, error) { + if err := store.ValidateInvokeConfigurationRequest(req); err != nil { + return nil, err + } + // find configuration + var confs []*pb.Configuration + err := h.storage.GetLatestConfigurationsByID(ctx, owner, []string{req.GetConfigurationId()}, func(v *store.Configuration) error { + c, err := v.GetLatest() + if err != nil { + return err + } + confs = append(confs, c.Clone()) + return nil + }) + if err != nil { + return nil, fmt.Errorf("cannot get configuration: %w", err) + } + if len(confs) < 1 { + return nil, fmt.Errorf("configuration not found: %v", req.GetConfigurationId()) + } + appliedConf, err := h.applyConfigurationOnDemand(ctx, confs[0], token, owner, req.GetDeviceId(), req.GetCorrelationId(), req.GetForce()) + if err != nil { + return nil, fmt.Errorf("cannot apply configuration: %w", err) + } + return appliedConf, nil +} + +func (h *ResourceUpdater) timeoutAppliedConfigurationPendingResource(ctx context.Context, owner, appliedConfigurationID, correlationID string, resourceID *commands.ResourceId) { + _, err := h.storage.UpdateAppliedConfigurationResource(ctx, owner, store.UpdateAppliedConfigurationResourceRequest{ + AppliedConfigurationID: appliedConfigurationID, + StatusFilter: []pb.AppliedConfiguration_Resource_Status{pb.AppliedConfiguration_Resource_PENDING}, + Resource: &pb.AppliedConfiguration_Resource{ + Href: resourceID.GetHref(), + CorrelationId: correlationID, + Status: pb.AppliedConfiguration_Resource_TIMEOUT, + ResourceUpdated: &events.ResourceUpdated{ + ResourceId: &commands.ResourceId{ + DeviceId: resourceID.GetDeviceId(), + Href: resourceID.GetHref(), + }, + Status: commands.Status_ERROR, + }, + }, + }) + if err != nil { + h.logger.Errorf("failed to timeout pending applied configuration for resource(%v): %w", resourceID.GetHref(), err) + } +} + +func (h *ResourceUpdater) TimeoutPendingResourceUpdates() { + h.logger.Debug("checking pending resource updates for timeout") + // get expired pending updates from the database + var pendingUpdates []*store.AppliedConfiguration + _, err := h.storage.GetPendingAppliedConfigurationResourceUpdates(h.ctx, true, func(ac *store.AppliedConfiguration) error { + pendingUpdates = append(pendingUpdates, ac) + return nil + }) + if err != nil { + h.logger.Errorf("cannot get expired pending resource updates: %v", err) + return + } + if len(pendingUpdates) == 0 { + return + } + + ctx := h.ctx + // cancel pending updates + for _, ac := range pendingUpdates { + for _, res := range ac.GetResources() { + resourceID := &commands.ResourceId{DeviceId: ac.GetDeviceId(), Href: res.GetHref()} + h.logger.Debugf("timeout for pending resource(%v) update reached", resourceID.GetHref()) + h.timeoutAppliedConfigurationPendingResource(ctx, ac.GetOwner(), ac.GetId(), res.GetCorrelationId(), resourceID) + } + } +} + +func (h *ResourceUpdater) CancelPendingResourceUpdates(ctx context.Context) error { + h.logger.Debug("canceling pending resource updates") + var pendingUpdates []*store.AppliedConfiguration + _, err := h.storage.GetPendingAppliedConfigurationResourceUpdates(h.ctx, false, func(ac *store.AppliedConfiguration) error { + pendingUpdates = append(pendingUpdates, ac) + return nil + }) + if err != nil { + return fmt.Errorf("cannot get pending applied configurations: %w", err) + } + if len(pendingUpdates) == 0 { + return nil + } + + var errs *multierror.Error + for _, ac := range pendingUpdates { + for _, res := range ac.GetResources() { + resourceID := &commands.ResourceId{DeviceId: ac.GetDeviceId(), Href: res.GetHref()} + err = h.cancelPendingResourceUpdate(ctx, resourceID, res.GetCorrelationId(), ac.GetConfigurationId().GetId()) + errs = multierror.Append(errs, err) + } + } + return errs.ErrorOrNil() +} + +func (h *ResourceUpdater) Close() error { + var errs *multierror.Error + if h.scheduler != nil { + err := h.scheduler.Shutdown() + errs = multierror.Append(errs, err) + } + err := h.raConn.Close() + errs = multierror.Append(errs, err) + return errs.ErrorOrNil() +} diff --git a/snippet-service/updater/resourceUpdater_test.go b/snippet-service/updater/resourceUpdater_test.go new file mode 100644 index 000000000..ffb1fbf41 --- /dev/null +++ b/snippet-service/updater/resourceUpdater_test.go @@ -0,0 +1,112 @@ +package updater_test + +import ( + "context" + "crypto/tls" + "testing" + "time" + + "github.com/plgd-dev/go-coap/v3/message" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/hub/v2/resource-aggregate/commands" + "github.com/plgd-dev/hub/v2/snippet-service/pb" + "github.com/plgd-dev/hub/v2/snippet-service/store" + "github.com/plgd-dev/hub/v2/snippet-service/test" + hubTest "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + hubTestService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestCleanUpExpiredUpdates(t *testing.T) { + deviceID := hubTest.MustFindDeviceByName(hubTest.TestDeviceName) + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + tearDown := hubTestService.SetUp(ctx, t) + defer tearDown() + + snippetCfg := test.MakeConfig(t) + const interval = time.Second + snippetCfg.Clients.ResourceUpdater.CleanUpExpiredUpdates = "*/1 * * * * *" + snippetCfg.Clients.ResourceUpdater.ExtendCronParserBySeconds = true + _, shutdownSnippetService := test.New(t, snippetCfg) + defer shutdownSnippetService() + + token := oauthTest.GetDefaultAccessToken(t) + + snippetClientConn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: hubTest.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = snippetClientConn.Close() + }() + snippetClient := pb.NewSnippetServiceClient(snippetClientConn) + + ctx = pkgGrpc.CtxWithToken(ctx, token) + + grpcClient := grpcgwTest.NewTestClient(t) + defer func() { + err = grpcClient.Close() + require.NoError(t, err) + }() + _, shutdownDevSim := hubTest.OnboardDevSim(ctx, t, grpcClient.GrpcGatewayClient(), deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, hubTest.GetAllBackendResourceLinks()) + defer shutdownDevSim() + + notExistingResourceHref := "/not/existing" + // configuration + // -> /not/existing -> { value: 42 } + conf, err := snippetClient.CreateConfiguration(ctx, &pb.Configuration{ + Name: "update", + Owner: oauthService.DeviceUserID, + Resources: []*pb.Configuration_Resource{ + { + Href: notExistingResourceHref, + Content: &commands.Content{ + ContentType: message.AppOcfCbor.String(), + Data: hubTest.EncodeToCbor(t, map[string]interface{}{ + "value": 42, + }), + }, + TimeToLive: int64(100 * time.Millisecond), + }, + }, + }) + require.NoError(t, err) + require.NotEmpty(t, conf.GetId()) + + // invoke configuration with long TimeToLive + resp, err := snippetClient.InvokeConfiguration(ctx, &pb.InvokeConfigurationRequest{ + ConfigurationId: conf.GetId(), + DeviceId: deviceID, + }) + require.NoError(t, err) + + time.Sleep(2 * interval) // 2 times the interval to guarantee that the clean up job has run at least once + + // check that all configurations are either in timeout or done state + s, cleanUpStore := test.NewStore(t) + defer cleanUpStore() + + appliedConfs := make(map[string]*pb.AppliedConfiguration) + err = s.GetAppliedConfigurations(ctx, oauthService.DeviceUserID, &pb.GetAppliedConfigurationsRequest{ + IdFilter: []string{resp.GetAppliedConfigurationId()}, + }, func(appliedConf *store.AppliedConfiguration) error { + appliedConfs[appliedConf.GetId()] = appliedConf.GetAppliedConfiguration().Clone() + return nil + }) + require.NoError(t, err) + require.Len(t, appliedConfs, 1) + appliedConf, ok := appliedConfs[resp.GetAppliedConfigurationId()] + require.True(t, ok) + for _, r := range appliedConf.GetResources() { + status := r.GetStatus() + require.True(t, pb.AppliedConfiguration_Resource_TIMEOUT == status || pb.AppliedConfiguration_Resource_DONE == status) + } +} diff --git a/snippet-service/updater/updateExecution.go b/snippet-service/updater/updateExecution.go new file mode 100644 index 000000000..77ab7f340 --- /dev/null +++ b/snippet-service/updater/updateExecution.go @@ -0,0 +1,73 @@ +package updater + +import "github.com/plgd-dev/hub/v2/snippet-service/pb" + +type executeByType int + +const ( + executeByTypeFindCondition executeByType = iota + executeByTypeCondition + executeByTypeOnDemand +) + +type appliedCondition struct { + id string + version uint64 + token string +} + +type appliedOnDemand struct { + token string +} + +type execution struct { + condition appliedCondition // executedBy = executeByTypeCondition + conditions []*pb.Condition // executedBy = executeByTypeFindCondition + onDemand appliedOnDemand // executedBy = executeByTypeOnDemand + force bool + executeBy executeByType +} + +func (e *execution) token() string { + if e.executeBy == executeByTypeOnDemand { + return e.onDemand.token + } + if e.executeBy == executeByTypeCondition { + return e.condition.token + } + return "" +} + +func (e *execution) setCondition(c appliedCondition) { + e.condition = c + e.conditions = nil + e.executeBy = executeByTypeCondition +} + +func (e *execution) setExecutedBy(ac *pb.AppliedConfiguration) { + if e.executeBy == executeByTypeOnDemand { + ac.ExecutedBy = pb.MakeExecutedByOnDemand() + return + } + if e.condition.id != "" { + ac.ExecutedBy = pb.MakeExecutedByConditionId(e.condition.id, e.condition.version) + return + } + firstCondition := e.conditions[0] + ac.ExecutedBy = pb.MakeExecutedByConditionId(firstCondition.GetId(), firstCondition.GetVersion()) +} + +type executionResult struct { + validUntil int64 + condition appliedCondition // executedBy = executeByTypeCondition or executeByTypeFindCondition + onDemand appliedOnDemand // executedBy = executeByTypeOnDemand + executedBy executeByType + err error +} + +func (er executionResult) token() string { + if er.executedBy == executeByTypeOnDemand { + return er.onDemand.token + } + return er.condition.token +} diff --git a/snippet-service/uri/uri.go b/snippet-service/uri/uri.go new file mode 100644 index 000000000..0c2b541f2 --- /dev/null +++ b/snippet-service/uri/uri.go @@ -0,0 +1,7 @@ +package uri + +const ( + HTTPIDFilterQueryKey = "httpIdFilter" + HTTPConfigurationIDFilter = "httpConfigurationIdFilter" + HTTPConditionIDFilter = "httpConditionIdFilter" +) diff --git a/test/config/config.go b/test/config/config.go index 80739a8af..cca1d37de 100644 --- a/test/config/config.go +++ b/test/config/config.go @@ -44,6 +44,8 @@ const ( CERTIFICATE_AUTHORITY_HOST = "localhost:20011" CERTIFICATE_AUTHORITY_HTTP_HOST = "localhost:20012" M2M_OAUTH_SERVER_HTTP_HOST = "localhost:20013" + SNIPPET_SERVICE_HOST = "localhost:20014" + SNIPPET_SERVICE_HTTP_HOST = "localhost:20015" GRPC_GW_HOST = "localhost:20005" C2C_CONNECTOR_HOST = "localhost:20006" C2C_CONNECTOR_DB = "cloud2cloudConnector" @@ -135,16 +137,20 @@ func MakeTLSServerConfig() server.Config { } } +func MakeAuthorizationConfig() grpcServer.AuthorizationConfig { + return grpcServer.AuthorizationConfig{ + OwnerClaim: OWNER_CLAIM, + Config: MakeValidatorConfig(), + } +} + func MakeGrpcServerConfig(address string) grpcServer.Config { return grpcServer.Config{ - Addr: address, - SendMsgSize: DefaultGrpcMaxMsgSize, - RecvMsgSize: DefaultGrpcMaxMsgSize, - TLS: MakeTLSServerConfig(), - Authorization: grpcServer.AuthorizationConfig{ - OwnerClaim: OWNER_CLAIM, - Config: MakeAuthorizationConfig(), - }, + Addr: address, + SendMsgSize: DefaultGrpcMaxMsgSize, + RecvMsgSize: DefaultGrpcMaxMsgSize, + TLS: MakeTLSServerConfig(), + Authorization: MakeAuthorizationConfig(), EnforcementPolicy: grpcServer.EnforcementPolicyConfig{ MinTime: time.Second * 5, PermitWithoutStream: true, @@ -244,7 +250,7 @@ func MakeEventsStoreCqlDBConfig() *cqldb.Config { } } -func MakeAuthorizationConfig() validator.Config { +func MakeValidatorConfig() validator.Config { return validator.Config{ Audience: http.HTTPS_SCHEME + OAUTH_MANAGER_AUDIENCE, Endpoints: []validator.AuthorityConfig{ diff --git a/test/http/request.go b/test/http/request.go index 0a3a266bf..5c116f832 100644 --- a/test/http/request.go +++ b/test/http/request.go @@ -102,6 +102,14 @@ func (c *RequestBuilder) DeviceId(deviceID string) *RequestBuilder { return c } +func (c *RequestBuilder) ID(id string) *RequestBuilder { + if id == "" { + return c + } + c.uriParams[IDKey] = id + return c +} + func (c *RequestBuilder) ResourceHref(resourceHref string) *RequestBuilder { if resourceHref == "" { return c @@ -121,6 +129,22 @@ func (c *RequestBuilder) SubscriptionID(subscriptionID string) *RequestBuilder { return c } +func (c *RequestBuilder) Version(version string) *RequestBuilder { + if version == "" { + return c + } + c.AddQuery(VersionKey, version) + return c +} + +func (c *RequestBuilder) IDFilter(idFilter []string) *RequestBuilder { + if len(idFilter) == 0 { + return c + } + c.AddQuery(IDFilterKey, idFilter...) + return c +} + func (c *RequestBuilder) SetQuery(value string) *RequestBuilder { c.query = value return c diff --git a/test/http/uri.go b/test/http/uri.go index f3ba99c3f..2d27cc2f3 100644 --- a/test/http/uri.go +++ b/test/http/uri.go @@ -3,9 +3,13 @@ package http const ( HTTPS_SCHEME = "https://" - DeviceIDKey = "deviceID" - ResourceHrefKey = "resourceHref" - SubscriptionIDKey = "subscriptionID" + IDKey = "id" + IDFilterKey = "idFilter" + DeviceIDKey = "deviceID" + ResourceHrefKey = "resourceHref" + SubscriptionIDKey = "subscriptionID" + ConfigurationIDKey = "configurationId" + VersionKey = "version" ContentQueryKey = "content" ) diff --git a/test/iotivity-lite/service/offboard_test.go b/test/iotivity-lite/service/offboard_test.go index 36941eeb4..f8c09c2df 100644 --- a/test/iotivity-lite/service/offboard_test.go +++ b/test/iotivity-lite/service/offboard_test.go @@ -55,7 +55,7 @@ func TestOffboard(t *testing.T) { require.Equal(t, 1, publishCount) singOffCount, ok := h.CallCounter.Data[iotService.SignOffKey] require.True(t, ok) - require.Positive(t, singOffCount, 0) + require.Positive(t, singOffCount) } coapShutdown := coapgwTest.SetUp(t, makeHandler, validateHandler) @@ -337,7 +337,7 @@ func TestOffboardWithSignInByRefreshToken(t *testing.T) { require.Greater(t, signInCount, 1) refreshCount, ok := h.CallCounter.Data[iotService.RefreshTokenKey] require.True(t, ok) - require.Positive(t, refreshCount, 0) + require.Positive(t, refreshCount) signOffCount, ok := h.CallCounter.Data[iotService.SignOffKey] require.True(t, ok) require.Equal(t, 1, signOffCount) diff --git a/test/iotivity-lite/service/republish_test.go b/test/iotivity-lite/service/republish_test.go index 7ee0a2093..a6367aae6 100644 --- a/test/iotivity-lite/service/republish_test.go +++ b/test/iotivity-lite/service/republish_test.go @@ -45,7 +45,7 @@ func TestRepublishAfterRefresh(t *testing.T) { require.Greater(t, signInCount, 1) refreshCount, ok := h.CallCounter.Data[iotService.RefreshTokenKey] require.True(t, ok) - require.Positive(t, refreshCount, 0) + require.Positive(t, refreshCount) publishCount, ok := h.CallCounter.Data[iotService.PublishKey] require.True(t, ok) require.Equal(t, 1, publishCount) diff --git a/test/pb/pendingCommand.go b/test/pb/pendingCommand.go index 54d5f9753..2a7865b5f 100644 --- a/test/pb/pendingCommand.go +++ b/test/pb/pendingCommand.go @@ -108,9 +108,7 @@ func InitPendingEvents(ctx context.Context, t *testing.T) (pb.GrpcGatewayClient, secureGWShutdown() createFn := func() { - createCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errC := c.CreateResource(createCtx, &pb.CreateResourceRequest{ + _, errC := c.CreateResource(ctx, &pb.CreateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1")), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -118,8 +116,9 @@ func InitPendingEvents(ctx context.Context, t *testing.T) (pb.GrpcGatewayClient, "power": 1, }), }, + Async: true, }) - require.Error(t, errC) + require.NoError(t, errC) } createFn() retrieveFn := func() { @@ -132,9 +131,7 @@ func InitPendingEvents(ctx context.Context, t *testing.T) (pb.GrpcGatewayClient, } retrieveFn() updateFn := func() { - updateCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errU := c.UpdateResource(updateCtx, &pb.UpdateResourceRequest{ + _, errU := c.UpdateResource(ctx, &pb.UpdateResourceRequest{ ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1")), Content: &pb.Content{ ContentType: message.AppOcfCbor.String(), @@ -142,17 +139,17 @@ func InitPendingEvents(ctx context.Context, t *testing.T) (pb.GrpcGatewayClient, "power": 1, }), }, + Async: true, }) - require.Error(t, errU) + require.NoError(t, errU) } updateFn() deleteFn := func() { - deleteCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, errD := c.DeleteResource(deleteCtx, &pb.DeleteResourceRequest{ + _, errD := c.DeleteResource(ctx, &pb.DeleteResourceRequest{ ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1")), + Async: true, }) - require.Error(t, errD) + require.NoError(t, errD) } deleteFn() updateDeviceMetadataFn := func() { diff --git a/test/test.go b/test/test.go index 605679d74..fad511dee 100644 --- a/test/test.go +++ b/test/test.go @@ -7,6 +7,7 @@ import ( "net" "os" "os/exec" + "slices" "sort" "strings" "testing" @@ -34,7 +35,6 @@ import ( "github.com/plgd-dev/hub/v2/grpc-gateway/client" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" isEvents "github.com/plgd-dev/hub/v2/identity-store/events" - pkgStrings "github.com/plgd-dev/hub/v2/pkg/strings" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" "github.com/plgd-dev/hub/v2/test/config" @@ -941,7 +941,7 @@ func DeviceIsBatchObservable(ctx context.Context, t *testing.T, deviceID string) require.NoError(t, err) require.Len(t, links, 1) return links[0].Policy.BitMask.Has(schema.Observable) && - pkgStrings.Contains(links[0].Interfaces, interfaces.OC_IF_B) + slices.Contains(links[0].Interfaces, interfaces.OC_IF_B) } func GetAllBackendResourceLinks() schema.ResourceLinks { From 46884219cd0a1a30de28ff178ada9ff5bd929500 Mon Sep 17 00:00:00 2001 From: Jozef Kralik Date: Tue, 9 Jul 2024 11:30:35 +0200 Subject: [PATCH 2/2] SnippetService UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SnippetService - templates init ( backup ) * Resources configuration * Conditions update * Versions columns * snippet service url from wellknown config * Applied configuration detail * Configurations Tab3 * Conditions Add page default value for deviceIdFilter * Table autosize * Resource statuses * ResourceToggleCreator canceled type * use form types * ResourceToggleCreator disabled content button od canceled status * Conditions visual improvements * dep fix * Conditions & AppliedConfiguration menu -> Tabs * Conditions add Configuration name * Tabs routes * Link fix * Conditions tab name fix * Conditions list link fix * Canceling pending commands from applied configuration * Pending commands multiple cancel * Invoking * Invoke update * snippet-service playwright testing * snippet-service playwright testing * DPS mock server * Generate token * m2mOauthClientType update * Generate Token for Conditions Add page * update condition wizard texts * fix text in selectConfigurationDescription * Storybook examples + tests * version Selector fix * Conditions - resources filters trim * Conditions - resources filters trim --------- Co-authored-by: Patrik MatiasÌŒko Co-authored-by: Daniel Adam --- dependency/googleapis | 2 +- http-gateway/web/package-lock.json | 24 +- .../web/packages/mock-server/package.json | 4 +- .../4f0d3d81-0e09-4f1e-9706-0997488011af.json | 17 + .../75eacc2f-ac28-4a42-a155-164393970ba4.json | 17 + .../data/dps/enrollment-groups/list/list.json | 35 ++ .../1c10a3b6-287c-11ec-ac2d-13054959c274.json | 56 ++ .../src/data/dps/linked-hubs/list/list.json | 58 ++ .../4a0d78b2-9a21-557d-ab08-bb4eaccfcbb0.json | 250 +++++++++ .../69f23484-171a-56a9-b300-89cd3007a70c.json | 250 +++++++++ .../dps/provisioning-records/list/list.json | 501 ++++++++++++++++++ .../src/data/pending-commands.json | 276 ++++++---- ...-48998f7d-2a70-46a4-8a68-745b69d55489.json | 272 ++++++++++ .../snippet-service/conditions/list/list.json | 18 + .../48998f7d-2a70-46a4-8a68-745b69d55489.json | 50 ++ .../configurations/list/list.json | 25 + .../web/packages/mock-server/src/index.js | 148 ++---- .../mock-server/src/routes/devices.js | 181 +++++++ .../packages/mock-server/src/routes/dps.js | 57 ++ .../mock-server/src/routes/snippet-service.js | 62 +++ .../web/packages/mock-server/src/utils.js | 35 ++ http-gateway/web/packages/shared-ui | 2 +- http-gateway/web/playwright/package-lock.json | 139 ++++- http-gateway/web/playwright/package.json | 6 +- .../configuratiions/deail.spec.ts | 17 + .../configuratiions/list.spec.ts | 13 + .../web/src/containers/App/App.i18n.ts | 16 + .../DetailHeaderLayout/DetailHeaderLayout.tsx | 76 +++ .../DetailHeaderLayout.types.ts | 27 + .../Common/DetailHeaderLayout/index.ts | 2 + .../Common/PageLayout/PageLayout.tsx | 12 +- .../Common/PageLayout/PageLayout.types.ts | 3 +- .../PageListTemplate/PageListTemplate.tsx | 167 ++++++ .../PageListTemplate.types.ts | 32 ++ .../containers/Common/TableList/TableList.tsx | 20 +- .../Common/TableList/TableList.types.ts | 7 +- .../EnrollmentGroups/DetailFormChunks.tsx | 2 +- .../ListPage/EnrollmentGroupsListPage.tsx | 2 +- .../LinkedHubs/DetailHeader/DetailHeader.tsx | 2 +- .../LinkedHubs/DetailPage/Tabs/Tab1/Tab1.tsx | 2 +- .../LinkNewHubPage/Steps/Step2/Step2.tsx | 2 +- .../LinkNewHubPage/Steps/Step4/Step4.tsx | 2 +- .../ListPage/LinkedHubsListPage.tsx | 2 +- .../DetailPage/DetailPage.tsx | 7 +- .../containers/DeviceProvisioning/hooks.ts | 2 +- .../DevicesDetailsPage/Tabs/Tab2/Tab2.tsx | 4 +- .../DevicesDetailsPage/Tabs/Tab4/Tab4.tsx | 6 +- .../src/containers/Devices/Devices.i18n.ts | 8 - .../web/src/containers/Devices/hooks.ts | 5 +- .../web/src/containers/Global.i18n.ts | 136 +++++ .../PendingCommands/DateFormat/DateFormat.tsx | 6 + .../PendingCommandDetailsModal.tsx | 2 +- .../PendingCommands/PendingCommands.i18n.ts | 12 + .../PendingCommandsList.tsx | 110 +++- .../PendingCommandsListPage.tsx | 23 +- .../src/containers/PendingCommands/hooks.ts | 22 +- .../DetailPage/DetailHeader.tsx | 80 +++ .../DetailPage/DetailPage.tsx | 155 ++++++ .../DetailPage/Tabs/Tab1/Tab1.tsx | 88 +++ .../DetailPage/Tabs/Tab1/Tab1.types.ts | 5 + .../DetailPage/Tabs/Tab1/index.ts | 2 + .../DetailPage/Tabs/Tab2/Tab2.tsx | 38 ++ .../DetailPage/Tabs/Tab2/Tab2.types.ts | 7 + .../DetailPage/Tabs/Tab2/index.ts | 2 + .../AppliedConfigurations/DetailPage/index.ts | 2 + .../ListPage/ListPage.tsx | 164 ++++++ .../AppliedConfigurations/ListPage/index.ts | 2 + .../AppliedConfigurations/index.ts | 2 + .../Conditions/AddPage/AddPage.tsx | 147 +++++ .../Conditions/AddPage/Steps/Step1/Step1.tsx | 86 +++ .../AddPage/Steps/Step1/Step1.types.ts | 8 + .../Conditions/AddPage/Steps/Step1/index.ts | 2 + .../Conditions/AddPage/Steps/Step2/Step2.tsx | 60 +++ .../AddPage/Steps/Step2/Step2.types.ts | 11 + .../Conditions/AddPage/Steps/Step2/index.ts | 2 + .../Conditions/AddPage/Steps/Step3/Step3.tsx | 146 +++++ .../AddPage/Steps/Step3/Step3.types.ts | 10 + .../Conditions/AddPage/Steps/Step3/index.ts | 2 + .../Conditions/AddPage/index.ts | 2 + .../Conditions/DetailPage/DetailForm.types.ts | 15 + .../Conditions/DetailPage/DetailHeader.tsx | 72 +++ .../Conditions/DetailPage/DetailPage.tsx | 219 ++++++++ .../Conditions/DetailPage/Tabs/Tab1/Tab1.tsx | 130 +++++ .../DetailPage/Tabs/Tab1/Tab1.types.ts | 11 + .../Conditions/DetailPage/Tabs/Tab1/index.ts | 2 + .../Conditions/DetailPage/Tabs/Tab2/Tab2.tsx | 39 ++ .../DetailPage/Tabs/Tab2/Tab2.types.ts | 14 + .../Conditions/DetailPage/Tabs/Tab2/index.ts | 2 + .../Conditions/DetailPage/Tabs/Tab3/Tab3.tsx | 104 ++++ .../DetailPage/Tabs/Tab3/Tab3.types.ts | 10 + .../Conditions/DetailPage/Tabs/Tab3/index.ts | 2 + .../Conditions/DetailPage/index.ts | 2 + .../Conditions/DetailPage/validationSchema.ts | 59 +++ .../Conditions/FomComponents.tsx | 272 ++++++++++ .../Conditions/ListPage/ListPage.tsx | 153 ++++++ .../Conditions/ListPage/index.ts | 2 + .../SnippetService/Conditions/index.ts | 2 + .../Conditions/validationSchema.ts | 26 + .../Configurations/AddPage/AddPage.tsx | 166 ++++++ .../Configurations/AddPage/index.ts | 2 + .../AddPage/validationSchema.ts | 26 + .../DetailPage/DetailHeader.tsx | 84 +++ .../Configurations/DetailPage/DetailPage.tsx | 269 ++++++++++ .../DetailPage/JsonConfigModal.tsx | 193 +++++++ .../DetailPage/Tabs/Tab1/Tab1.tsx | 247 +++++++++ .../DetailPage/Tabs/Tab1/Tab1.types.ts | 16 + .../DetailPage/Tabs/Tab1/index.ts | 2 + .../DetailPage/Tabs/Tab2/Tab2.tsx | 113 ++++ .../DetailPage/Tabs/Tab2/Tab2.types.ts | 5 + .../DetailPage/Tabs/Tab2/index.ts | 2 + .../DetailPage/Tabs/Tab3/Tab3.tsx | 171 ++++++ .../DetailPage/Tabs/Tab3/Tab3.types.ts | 5 + .../DetailPage/Tabs/Tab3/index.ts | 2 + .../Configurations/DetailPage/index.ts | 2 + .../DetailPage/validationSchema.ts | 26 + .../InvokeModal/InvokeModal.tsx | 150 ++++++ .../InvokeModal/InvokeModal.types.ts | 5 + .../Configurations/InvokeModal/index.ts | 2 + .../Configurations/ListPage/ListPage.tsx | 165 ++++++ .../Configurations/ListPage/index.ts | 2 + .../SnippetService/Configurations/index.ts | 2 + .../SnippetService/ServiceSnippet.types.ts | 58 ++ .../SnippetService/SnippetService.i18n.ts | 470 ++++++++++++++++ .../containers/SnippetService/constants.ts | 34 ++ .../src/containers/SnippetService/hooks.ts | 295 +++++++++++ .../web/src/containers/SnippetService/rest.ts | 168 ++++++ .../src/containers/SnippetService/utils.tsx | 143 +++++ http-gateway/web/src/notificationId.ts | 38 ++ http-gateway/web/src/routes.tsx | 166 +++++- http-gateway/web/src/testId.ts | 39 ++ 130 files changed, 8121 insertions(+), 340 deletions(-) create mode 100644 http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/detail/4f0d3d81-0e09-4f1e-9706-0997488011af.json create mode 100644 http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/detail/75eacc2f-ac28-4a42-a155-164393970ba4.json create mode 100644 http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/list/list.json create mode 100644 http-gateway/web/packages/mock-server/src/data/dps/linked-hubs/detail/1c10a3b6-287c-11ec-ac2d-13054959c274.json create mode 100644 http-gateway/web/packages/mock-server/src/data/dps/linked-hubs/list/list.json create mode 100644 http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/detail/4a0d78b2-9a21-557d-ab08-bb4eaccfcbb0.json create mode 100644 http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/detail/69f23484-171a-56a9-b300-89cd3007a70c.json create mode 100644 http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/list/list.json create mode 100644 http-gateway/web/packages/mock-server/src/data/snippet-service/applied-configurations/list/httpConfigurationIdFilter-48998f7d-2a70-46a4-8a68-745b69d55489.json create mode 100644 http-gateway/web/packages/mock-server/src/data/snippet-service/conditions/list/list.json create mode 100644 http-gateway/web/packages/mock-server/src/data/snippet-service/configurations/detail/48998f7d-2a70-46a4-8a68-745b69d55489.json create mode 100644 http-gateway/web/packages/mock-server/src/data/snippet-service/configurations/list/list.json create mode 100644 http-gateway/web/packages/mock-server/src/routes/devices.js create mode 100644 http-gateway/web/packages/mock-server/src/routes/dps.js create mode 100644 http-gateway/web/packages/mock-server/src/routes/snippet-service.js create mode 100644 http-gateway/web/packages/mock-server/src/utils.js create mode 100644 http-gateway/web/playwright/tests/snippet-service/configuratiions/deail.spec.ts create mode 100644 http-gateway/web/playwright/tests/snippet-service/configuratiions/list.spec.ts create mode 100644 http-gateway/web/src/containers/Common/DetailHeaderLayout/DetailHeaderLayout.tsx create mode 100644 http-gateway/web/src/containers/Common/DetailHeaderLayout/DetailHeaderLayout.types.ts create mode 100644 http-gateway/web/src/containers/Common/DetailHeaderLayout/index.ts create mode 100644 http-gateway/web/src/containers/Common/PageListTemplate/PageListTemplate.tsx create mode 100644 http-gateway/web/src/containers/Common/PageListTemplate/PageListTemplate.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/DetailPage/DetailHeader.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/DetailPage/DetailPage.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/DetailPage/Tabs/Tab1/Tab1.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/DetailPage/Tabs/Tab1/Tab1.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/DetailPage/Tabs/Tab1/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/DetailPage/Tabs/Tab2/Tab2.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/DetailPage/Tabs/Tab2/Tab2.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/DetailPage/Tabs/Tab2/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/DetailPage/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/ListPage/ListPage.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/ListPage/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/AppliedConfigurations/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/AddPage/AddPage.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/AddPage/Steps/Step1/Step1.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/AddPage/Steps/Step1/Step1.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/AddPage/Steps/Step1/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/AddPage/Steps/Step2/Step2.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/AddPage/Steps/Step2/Step2.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/AddPage/Steps/Step2/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/AddPage/Steps/Step3/Step3.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/AddPage/Steps/Step3/Step3.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/AddPage/Steps/Step3/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/AddPage/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/DetailForm.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/DetailHeader.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/DetailPage.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/Tabs/Tab1/Tab1.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/Tabs/Tab1/Tab1.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/Tabs/Tab1/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/Tabs/Tab2/Tab2.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/Tabs/Tab2/Tab2.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/Tabs/Tab2/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/Tabs/Tab3/Tab3.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/Tabs/Tab3/Tab3.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/Tabs/Tab3/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/DetailPage/validationSchema.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/FomComponents.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/ListPage/ListPage.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/ListPage/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Conditions/validationSchema.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/AddPage/AddPage.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/AddPage/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/AddPage/validationSchema.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/DetailHeader.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/DetailPage.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/JsonConfigModal.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/Tabs/Tab1/Tab1.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/Tabs/Tab1/Tab1.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/Tabs/Tab1/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/Tabs/Tab2/Tab2.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/Tabs/Tab2/Tab2.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/Tabs/Tab2/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/Tabs/Tab3/Tab3.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/Tabs/Tab3/Tab3.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/Tabs/Tab3/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/DetailPage/validationSchema.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/InvokeModal/InvokeModal.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/InvokeModal/InvokeModal.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/InvokeModal/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/ListPage/ListPage.tsx create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/ListPage/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/Configurations/index.ts create mode 100644 http-gateway/web/src/containers/SnippetService/ServiceSnippet.types.ts create mode 100644 http-gateway/web/src/containers/SnippetService/SnippetService.i18n.ts create mode 100644 http-gateway/web/src/containers/SnippetService/constants.ts create mode 100644 http-gateway/web/src/containers/SnippetService/hooks.ts create mode 100644 http-gateway/web/src/containers/SnippetService/rest.ts create mode 100644 http-gateway/web/src/containers/SnippetService/utils.tsx diff --git a/dependency/googleapis b/dependency/googleapis index 821a6ddff..7976ffadc 160000 --- a/dependency/googleapis +++ b/dependency/googleapis @@ -1 +1 @@ -Subproject commit 821a6ddff3ab55d2639dc2ca95a07f0e0b957a26 +Subproject commit 7976ffadc0f21ee9149708c0c97ef000e15de1eb diff --git a/http-gateway/web/package-lock.json b/http-gateway/web/package-lock.json index 5ecb906f0..0e5358ac4 100644 --- a/http-gateway/web/package-lock.json +++ b/http-gateway/web/package-lock.json @@ -36963,7 +36963,9 @@ "license": "ISC", "dependencies": { "cors": "^2.8.5", - "express-validator": "^7.1.0" + "escape-html": "^1.0.3", + "express-validator": "^7.1.0", + "lodash": "^4.17.21" }, "devDependencies": { "express": "^4.18.2" @@ -37051,6 +37053,7 @@ "react-table": "^7.8.0", "react-toastify": "^9.1.3", "react-transition-group": "^4.4.5", + "recoil": "^0.7.7", "swiper": "^10.2.0", "tsconfig-paths-webpack-plugin": "^4.1.0", "typescript": "^4.9.5", @@ -37094,7 +37097,7 @@ "eslint-plugin-import": "^2.29.0", "eslint-plugin-jest": "^27.6.0", "eslint-plugin-jsx-a11y": "^6.8.0", - "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.8.0", @@ -37110,7 +37113,7 @@ "npm-run-all": "^4.1.5", "path": "^0.12.7", "postcss-modules": "^6.0.0", - "prettier": "^3.1.0", + "prettier": "^3.3.2", "prop-types": "^15.8.1", "resize-observer-polyfill": "^1.5.1", "rimraf": "^5.0.5", @@ -37125,6 +37128,21 @@ "yargs": "^17.7.2" } }, + "packages/shared-ui/node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "packages/shared-ui/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", diff --git a/http-gateway/web/packages/mock-server/package.json b/http-gateway/web/packages/mock-server/package.json index 460b398b3..2fdafde62 100644 --- a/http-gateway/web/packages/mock-server/package.json +++ b/http-gateway/web/packages/mock-server/package.json @@ -14,6 +14,8 @@ }, "dependencies": { "cors": "^2.8.5", - "express-validator": "^7.1.0" + "escape-html": "^1.0.3", + "express-validator": "^7.1.0", + "lodash": "^4.17.21" } } diff --git a/http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/detail/4f0d3d81-0e09-4f1e-9706-0997488011af.json b/http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/detail/4f0d3d81-0e09-4f1e-9706-0997488011af.json new file mode 100644 index 000000000..a2f174a3d --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/detail/4f0d3d81-0e09-4f1e-9706-0997488011af.json @@ -0,0 +1,17 @@ +{ + "result": { + "id": "4f0d3d81-0e09-4f1e-9706-0997488011af", + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "attestationMechanism": { + "x509": { + "certificateChain": "/secrets/enrollmentgroups/x509/4f0d3d81-0e09-4f1e-9706-0997488011af/certificatechain.crt", + "leadCertificateName": "MfgRootCA", + "expiredCertificateEnabled": false + } + }, + "hubIds": [ + "1c10a3b6-287c-11ec-ac2d-13054959c274" + ], + "name": "4f0d3d81-0e09-4f1e-9706-0997488011af" + } +} \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/detail/75eacc2f-ac28-4a42-a155-164393970ba4.json b/http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/detail/75eacc2f-ac28-4a42-a155-164393970ba4.json new file mode 100644 index 000000000..5092c07f9 --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/detail/75eacc2f-ac28-4a42-a155-164393970ba4.json @@ -0,0 +1,17 @@ +{ + "result": { + "id": "75eacc2f-ac28-4a42-a155-164393970ba4", + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "attestationMechanism": { + "x509": { + "certificateChain": "/secrets/enrollmentgroups/x509/75eacc2f-ac28-4a42-a155-164393970ba4/certificatechain.crt", + "leadCertificateName": "root.ca.plgd.cloud", + "expiredCertificateEnabled": false + } + }, + "hubIds": [ + "1c10a3b6-287c-11ec-ac2d-13054959c274" + ], + "name": "75eacc2f-ac28-4a42-a155-164393970ba4" + } +} \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/list/list.json b/http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/list/list.json new file mode 100644 index 000000000..fdd469730 --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/dps/enrollment-groups/list/list.json @@ -0,0 +1,35 @@ +[ + { + "result": { + "id": "75eacc2f-ac28-4a42-a155-164393970ba4", + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "attestationMechanism": { + "x509": { + "certificateChain": "/secrets/enrollmentgroups/x509/75eacc2f-ac28-4a42-a155-164393970ba4/certificatechain.crt", + "leadCertificateName": "root.ca.plgd.cloud", + "expiredCertificateEnabled": false + } + }, + "hubIds": [ + "1c10a3b6-287c-11ec-ac2d-13054959c274" + ], + "name": "75eacc2f-ac28-4a42-a155-164393970ba4" + } + },{ + "result": { + "hubIds": [ + "1c10a3b6-287c-11ec-ac2d-13054959c274" + ], + "name": "4f0d3d81-0e09-4f1e-9706-0997488011af", + "id": "4f0d3d81-0e09-4f1e-9706-0997488011af", + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "attestationMechanism": { + "x509": { + "certificateChain": "/secrets/enrollmentgroups/x509/4f0d3d81-0e09-4f1e-9706-0997488011af/certificatechain.crt", + "leadCertificateName": "MfgRootCA", + "expiredCertificateEnabled": false + } + } + } +} +] \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/data/dps/linked-hubs/detail/1c10a3b6-287c-11ec-ac2d-13054959c274.json b/http-gateway/web/packages/mock-server/src/data/dps/linked-hubs/detail/1c10a3b6-287c-11ec-ac2d-13054959c274.json new file mode 100644 index 000000000..b857b8c11 --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/dps/linked-hubs/detail/1c10a3b6-287c-11ec-ac2d-13054959c274.json @@ -0,0 +1,56 @@ +{ + "result": { + "name": "1c10a3b6-287c-11ec-ac2d-13054959c274", + "hubId": "1c10a3b6-287c-11ec-ac2d-13054959c274", + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "id": "1c10a3b6-287c-11ec-ac2d-13054959c274", + "gateways": [ + "coaps+tcp://try.plgd.cloud:5684" + ], + "certificateAuthority": { + "grpc": { + "tls": { + "caPool": [ + "/certs/client/ca.crt" + ], + "key": "/certs/client/tls.key", + "cert": "/certs/client/tls.crt", + "useSystemCaPool": true + }, + "address": "csr.try.plgd.cloud:443", + "keepAlive": { + "timeout": "20000000000", + "permitWithoutStream": true, + "time": "10000000000" + } + } + }, + "authorization": { + "ownerClaim": "owner-id", + "provider": { + "clientId": "p2elIQo8ZOQZ7pSgAfi0JyebeE6uwWNK", + "scopes": [ + "openid" + ], + "clientSecret": "/secrets/enrollmentgroups/hubs/oauth/4f0d3d81-0e09-4f1e-9706-0997488011af/clientsecret", + "http": { + "maxConnsPerHost": 32, + "maxIdleConnsPerHost": 16, + "idleConnTimeout": "30000000000", + "timeout": "10000000000", + "tls": { + "key": "/certs/client/tls.key", + "cert": "/certs/client/tls.crt", + "useSystemCaPool": true, + "caPool": [ + "/certs/client/ca.crt" + ] + }, + "maxIdleConns": 16 + }, + "name": "plgd.dps", + "authority": "https://auth.plgd.cloud/realms/shared" + } + } + } +} \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/data/dps/linked-hubs/list/list.json b/http-gateway/web/packages/mock-server/src/data/dps/linked-hubs/list/list.json new file mode 100644 index 000000000..63c7fc522 --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/dps/linked-hubs/list/list.json @@ -0,0 +1,58 @@ +[ + { + "result": { + "certificateAuthority": { + "grpc": { + "address": "csr.try.plgd.cloud:443", + "keepAlive": { + "time": "10000000000", + "timeout": "20000000000", + "permitWithoutStream": true + }, + "tls": { + "cert": "/certs/client/tls.crt", + "useSystemCaPool": true, + "caPool": [ + "/certs/client/ca.crt" + ], + "key": "/certs/client/tls.key" + } + } + }, + "authorization": { + "provider": { + "clientSecret": "/secrets/enrollmentgroups/hubs/oauth/4f0d3d81-0e09-4f1e-9706-0997488011af/clientsecret", + "http": { + "maxIdleConns": 16, + "maxConnsPerHost": 32, + "maxIdleConnsPerHost": 16, + "idleConnTimeout": "30000000000", + "timeout": "10000000000", + "tls": { + "caPool": [ + "/certs/client/ca.crt" + ], + "key": "/certs/client/tls.key", + "cert": "/certs/client/tls.crt", + "useSystemCaPool": true + } + }, + "name": "plgd.dps", + "authority": "https://auth.plgd.cloud/realms/shared", + "clientId": "p2elIQo8ZOQZ7pSgAfi0JyebeE6uwWNK", + "scopes": [ + "openid" + ] + }, + "ownerClaim": "owner-id" + }, + "name": "1c10a3b6-287c-11ec-ac2d-13054959c274", + "hubId": "1c10a3b6-287c-11ec-ac2d-13054959c274", + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "id": "1c10a3b6-287c-11ec-ac2d-13054959c274", + "gateways": [ + "coaps+tcp://try.plgd.cloud:5684" + ] + } + } +] \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/detail/4a0d78b2-9a21-557d-ab08-bb4eaccfcbb0.json b/http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/detail/4a0d78b2-9a21-557d-ab08-bb4eaccfcbb0.json new file mode 100644 index 000000000..95dd44eab --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/detail/4a0d78b2-9a21-557d-ab08-bb4eaccfcbb0.json @@ -0,0 +1,250 @@ +{ + "result": { + "deviceId": "bcd30976-10e4-4145-5282-eb50ee55dfae", + "enrollmentGroupId": "4f0d3d81-0e09-4f1e-9706-0997488011af", + "attestation": { + "date": "1717157743336135946", + "x509": { + "certificatePem": "-----BEGIN CERTIFICATE-----\nMIIBhTCCASugAwIBAgIRAJ+UC+MPMl6jc9FVeHZEgB4wCgYIKoZIzj0EAwIwFDES\nMBAGA1UEAxMJTWZnUm9vdENBMCAXDTIzMTIxMTEyNTIwMVoYDzIxMjMxMTE3MTI1\nMjAxWjATMREwDwYDVQQDEwhNZmdDZXJ0MTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABFU2BHkqcNsiN68hGoFGzk+QtK78su3HpObu0U6Yhq5tnbGffQ4oTcel4Lhx\nlW5FHWUPI/l2LV9Ej/i3DSaurs6jXTBbMB8GA1UdIwQYMBaAFFyGFDlvMWlF0wb8\n0yDIfbRH7lTPMAwGA1UdEwQFMAMBAQAwCwYDVR0PBAQDAgOIMB0GA1UdJQQWMBQG\nCCsGAQUFBwMCBggrBgEFBQcDATAKBggqhkjOPQQDAgNIADBFAiBM31BMFp/ImqVt\nwH55aaqYSYp00pabSyIbKwFle6B+2wIhAMqXjZ3zoEECNU7zUo/hpYPTqanak2t6\ndPVwtlKcRlPx\n-----END CERTIFICATE-----\n", + "commonName": "MfgCert1" + } + }, + "acl": { + "status": { + "date": "1717157743480774484", + "coapCode": 69 + }, + "accessControlList": [ + { + "deviceSubject": { + "deviceId": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "permissions": [ + "CREATE", + "READ", + "WRITE", + "DELETE", + "NOTIFY" + ], + "resources": [ + { + "href": "/CoapCloudConfResURI", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/sec/sp" + }, + { + "href": "/x.plgd.dev/time", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "wildcard": "NONE", + "href": "/oc/con", + "interfaces": [ + "*" + ] + }, + { + "href": "/oc/swu", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/mnt" + }, + { + "href": "/x.plgd.dev/time", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONCFG_ALL" + } + ] + }, + { + "connectionSubject": { + "type": "ANON_CLEAR" + }, + "permissions": [ + "READ" + ], + "resources": [ + { + "href": "/oic/d", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oic/p", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oic/res", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/sec/sdi" + }, + { + "href": "/oic/sec/doxm", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + } + ] + }, + { + "deviceSubject": { + "deviceId": "1c10a3b6-287c-11ec-ac2d-13054959c274" + }, + "permissions": [ + "CREATE", + "READ", + "WRITE", + "DELETE", + "NOTIFY" + ], + "resources": [ + { + "href": "/oc/con", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oc/swu", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oic/mnt", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/x.plgd.dev/time" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONCFG_ALL" + } + ] + } + ] + }, + "cloud": { + "status": { + "date": "1717157743426800468", + "coapCode": 68 + }, + "providerName": "plgd.dps", + "gateways": [ + { + "id": "1c10a3b6-287c-11ec-ac2d-13054959c274", + "uri": "coaps+tcp://try.plgd.cloud:5684" + } + ], + "selectedGateway": 0 + }, + "ownership": { + "status": { + "date": "1717157743352900277", + "coapCode": 69 + }, + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "localEndpoints": [ + "coap://10.110.110.12:45396", + "coaps://10.110.110.12:52852", + "coap+tcp://10.110.110.12:51863", + "coaps+tcp://10.110.110.12:38145" + ], + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "id": "4a0d78b2-9a21-557d-ab08-bb4eaccfcbb0", + "creationDate": "1716656687662199144", + "credential": { + "status": { + "date": "1717157743461436598", + "coapCode": 68 + }, + "identityCertificatePem": "-----BEGIN CERTIFICATE-----\nMIIBojCCAUigAwIBAgIQIm1TIO0PH7O+w+LXTfV1KTAKBggqhkjOPQQDAjASMRAw\nDgYDVQQDEwdwbGdkLWNhMB4XDTI0MDUzMTExMTU0M1oXDTI1MDUyNTE3MDI1MVow\nNDEyMDAGA1UEAxMpdXVpZDpiY2QzMDk3Ni0xMGU0LTQxNDUtNTI4Mi1lYjUwZWU1\nNWRmYWUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQK9W+P4MDWdABFVSgZOWnW\nDGEkU8abmg2zVW1MlGPZF4YKpKWEUIoqeVxpm5D/yKtzG15uuvx3U2oU3UbRGL5t\no14wXDAOBgNVHQ8BAf8EBAMCA4gwKQYDVR0lBCIwIAYIKwYBBQUHAwIGCCsGAQUF\nBwMBBgorBgEEAYLefAEGMB8GA1UdIwQYMBaAFCM9S7rizy2ArYhiARButOX7DTPa\nMAoGCCqGSM49BAMCA0gAMEUCIBMdkdXzDBJgThKP3Q1hTV/XzigwlSe7+YlXoINl\ngW8DAiEA7f9ghvAZzBbep1sMDmcTFFLquzAPNsRBSCkSquyYUNQ=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBkDCCATagAwIBAgIQFq/ergts3Jlaxk3N92FfzTAKBggqhkjOPQQDAjAdMRsw\nGQYDVQQDExJyb290LmNhLnBsZ2QuY2xvdWQwHhcNMjQwNTI1MTcwMjUxWhcNMjUw\nNTI1MTcwMjUxWjASMRAwDgYDVQQDEwdwbGdkLWNhMFkwEwYHKoZIzj0CAQYIKoZI\nzj0DAQcDQgAE5s5zak9SL6OCMM49iyJYpNvnHly3UVdTe4jaAxmMS2YM63e68Cix\nevhl849SzCnpoD9yJzFA2HmhEc82IVt66KNjMGEwDgYDVR0PAQH/BAQDAgGGMA8G\nA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCM9S7rizy2ArYhiARButOX7DTPaMB8G\nA1UdIwQYMBaAFELK01Gtmvz99kTvIM5idzOLxerIMAoGCCqGSM49BAMCA0gAMEUC\nIG4Cnqkb+atbFIAeTFyIR4v3ABi0CjPub07xnt/bvN6FAiEAxgyW5sibfw7z3/xc\nQnA0w68sKmVRZhvfUNMYkqbk/2w=\n-----END CERTIFICATE-----\n", + "credentials": [ + { + "type": [ + "ASYMMETRIC_SIGNING_WITH_CERTIFICATE" + ], + "usage": "CERT", + "publicData": { + "data": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJvakNDQVVpZ0F3SUJBZ0lRSW0xVElPMFBIN08rdytMWFRmVjFLVEFLQmdncWhrak9QUVFEQWpBU01SQXcKRGdZRFZRUURFd2R3Ykdka0xXTmhNQjRYRFRJME1EVXpNVEV4TVRVME0xb1hEVEkxTURVeU5URTNNREkxTVZvdwpOREV5TURBR0ExVUVBeE1wZFhWcFpEcGlZMlF6TURrM05pMHhNR1UwTFRReE5EVXROVEk0TWkxbFlqVXdaV1UxCk5XUm1ZV1V3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFLOVcrUDRNRFdkQUJGVlNnWk9XblcKREdFa1U4YWJtZzJ6VlcxTWxHUFpGNFlLcEtXRVVJb3FlVnhwbTVEL3lLdHpHMTV1dXZ4M1Uyb1UzVWJSR0w1dApvMTR3WERBT0JnTlZIUThCQWY4RUJBTUNBNGd3S1FZRFZSMGxCQ0l3SUFZSUt3WUJCUVVIQXdJR0NDc0dBUVVGCkJ3TUJCZ29yQmdFRUFZTGVmQUVHTUI4R0ExVWRJd1FZTUJhQUZDTTlTN3JpenkyQXJZaGlBUkJ1dE9YN0RUUGEKTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUJNZGtkWHpEQkpnVGhLUDNRMWhUVi9Yemlnd2xTZTcrWWxYb0lObApnVzhEQWlFQTdmOWdodkFaekJiZXAxc01EbWNURkZMcXV6QVBOc1JCU0NrU3F1eVlVTlE9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrRENDQVRhZ0F3SUJBZ0lRRnEvZXJndHMzSmxheGszTjkyRmZ6VEFLQmdncWhrak9QUVFEQWpBZE1Sc3cKR1FZRFZRUURFeEp5YjI5MExtTmhMbkJzWjJRdVkyeHZkV1F3SGhjTk1qUXdOVEkxTVRjd01qVXhXaGNOTWpVdwpOVEkxTVRjd01qVXhXakFTTVJBd0RnWURWUVFERXdkd2JHZGtMV05oTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJCnpqMERBUWNEUWdBRTVzNXphazlTTDZPQ01NNDlpeUpZcE52bkhseTNVVmRUZTRqYUF4bU1TMllNNjNlNjhDaXgKZXZobDg0OVN6Q25wb0Q5eUp6RkEySG1oRWM4MklWdDY2S05qTUdFd0RnWURWUjBQQVFIL0JBUURBZ0dHTUE4RwpBMVVkRXdFQi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZDTTlTN3JpenkyQXJZaGlBUkJ1dE9YN0RUUGFNQjhHCkExVWRJd1FZTUJhQUZFTEswMUd0bXZ6OTlrVHZJTTVpZHpPTHhlcklNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUMKSUc0Q25xa2IrYXRiRklBZVRGeUlSNHYzQUJpMENqUHViMDd4bnQvYnZONkZBaUVBeGd5VzVzaWJmdzd6My94YwpRbkEwdzY4c0ttVlJaaHZmVU5NWWtxYmsvMnc9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + "encoding": "PEM" + }, + "id": "0", + "subject": "bcd30976-10e4-4145-5282-eb50ee55dfae" + }, + { + "publicData": { + "data": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJmVENDQVNPZ0F3SUJBZ0lSQUlwR0Jrb0U2ZXdwdFJMT1FaMXZrb1V3Q2dZSUtvWkl6ajBFQXdJd0hURWIKTUJrR0ExVUVBeE1TY205dmRDNWpZUzV3Ykdka0xtTnNiM1ZrTUNBWERUSXpNVEF4T1RBNE5EZ3hOVm9ZRHpJeApNak13T1RJMU1EZzBPREUxV2pBZE1Sc3dHUVlEVlFRREV4SnliMjkwTG1OaExuQnNaMlF1WTJ4dmRXUXdXVEFUCkJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFtMHgrMTZCdWt2ditIV0FlcnkvMG8vVlFBTzhib3hzNHcKT2lhWFRybDBhbi9iUkR6REFwc1J2cDY5SHdJY2haS0FjZTVPQ3JkZFNpVVJOdDZRZWYwQ28wSXdRREFPQmdOVgpIUThCQWY4RUJBTUNBUVl3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVUXNyVFVhMmEvUDMyClJPOGd6bUozTTR2RjZzZ3dDZ1lJS29aSXpqMEVBd0lEU0FBd1JRSWdOMHBnS1N4d2RvUklkV1RxaTdUcDd0eVAKb05LNERxeElsWWJuRUlJb3BlZ0NJUURsd2FUMmg2RUtaM1R3SHFBcTU1UXl3TDM3RzJwdjVKOE03dDFFcVBnQQpZQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + "encoding": "PEM" + }, + "id": "0", + "subject": "beb32777-9680-4f42-8761-350eebe76a85", + "usage": "TRUST_CA", + "type": [ + "ASYMMETRIC_SIGNING_WITH_CERTIFICATE" + ] + } + ] + }, + "plgdTime": { + "date": "1717157743283612068", + "coapCode": 69 + } + } +} \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/detail/69f23484-171a-56a9-b300-89cd3007a70c.json b/http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/detail/69f23484-171a-56a9-b300-89cd3007a70c.json new file mode 100644 index 000000000..7c7b62a56 --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/detail/69f23484-171a-56a9-b300-89cd3007a70c.json @@ -0,0 +1,250 @@ +{ + "result": { + "localEndpoints": [ + "coap://10.110.110.12:41885", + "coaps://10.110.110.12:49997", + "coap+tcp://10.110.110.12:35389", + "coaps+tcp://10.110.110.12:33711" + ], + "deviceId": "d9dde79c-3916-433b-4789-ca10aad6fc4a", + "creationDate": "1716652467413672050", + "attestation": { + "date": "1719474938251929779", + "x509": { + "certificatePem": "-----BEGIN CERTIFICATE-----\nMIIBgjCCASmgAwIBAgIQdYzG5cWdt18FWrG0WxBqNzAKBggqhkjOPQQDAjAUMRIw\nEAYDVQQDEwlNZmdSb290Q0EwIBcNMjMxMTA2MDcyNzU5WhgPMjEyMzEwMTMwNzI3\nNTlaMBIxEDAOBgNVBAMTB01mZ0NlcnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\nAAQMkz/uQi1sbMmuxgagH/A5II6CcxZz9sWpSjuxLEqPNEbL021yUaQX6/U2GBYs\npFVWzGPWIog30mgGq/jvDyQKo10wWzAfBgNVHSMEGDAWgBRchhQ5bzFpRdMG/NMg\nyH20R+5UzzAMBgNVHRMEBTADAQEAMAsGA1UdDwQEAwIDiDAdBgNVHSUEFjAUBggr\nBgEFBQcDAgYIKwYBBQUHAwEwCgYIKoZIzj0EAwIDRwAwRAIgbqEU4iOhHpD4YnjC\nO5J8lTIw+RbZ+r9uZD1K2fz4ZfkCIGFzidJQAmBSPVvEdK4X4CSf76BjEoRwXxm2\nKaeI8Vkl\n-----END CERTIFICATE-----\n", + "commonName": "MfgCert" + } + }, + "cloud": { + "status": { + "coapCode": 68, + "date": "1719474938285092512" + }, + "providerName": "plgd.dps", + "gateways": [ + { + "uri": "coaps+tcp://try.plgd.cloud:5684", + "id": "1c10a3b6-287c-11ec-ac2d-13054959c274" + } + ], + "selectedGateway": 0 + }, + "ownership": { + "status": { + "date": "1719474938266133666", + "coapCode": 69 + }, + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "plgdTime": { + "date": "1719474938192355476", + "coapCode": 69 + }, + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "id": "69f23484-171a-56a9-b300-89cd3007a70c", + "enrollmentGroupId": "4f0d3d81-0e09-4f1e-9706-0997488011af", + "credential": { + "status": { + "date": "1719474938324358862", + "coapCode": 68 + }, + "identityCertificatePem": "-----BEGIN CERTIFICATE-----\nMIIBpDCCAUmgAwIBAgIRAPJNdAgYEQhNPjTxlF3/1v0wCgYIKoZIzj0EAwIwEjEQ\nMA4GA1UEAxMHcGxnZC1jYTAeFw0yNDA2MjcwNjU1MzhaFw0yNTA1MjUxNzAyNTFa\nMDQxMjAwBgNVBAMTKXV1aWQ6ZDlkZGU3OWMtMzkxNi00MzNiLTQ3ODktY2ExMGFh\nZDZmYzRhMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaWg0Wurmnrl8LTvARHc+\nCu8eiLBIsKXp/RALqr2htsFK0jlkStpuxMWfSofbTiY/8gD1YmM3Q833CL0Sg+C5\nZaNeMFwwDgYDVR0PAQH/BAQDAgOIMCkGA1UdJQQiMCAGCCsGAQUFBwMCBggrBgEF\nBQcDAQYKKwYBBAGC3nwBBjAfBgNVHSMEGDAWgBQjPUu64s8tgK2IYgEQbrTl+w0z\n2jAKBggqhkjOPQQDAgNJADBGAiEA/bvt26yZJozHeXJGusuH4HLZd4eAkQy7Bc9x\nSmt3UukCIQDx9g2DUPECP/20MV18FKkitUAN3Ed4tB/gmX17tCBuQw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBkDCCATagAwIBAgIQFq/ergts3Jlaxk3N92FfzTAKBggqhkjOPQQDAjAdMRsw\nGQYDVQQDExJyb290LmNhLnBsZ2QuY2xvdWQwHhcNMjQwNTI1MTcwMjUxWhcNMjUw\nNTI1MTcwMjUxWjASMRAwDgYDVQQDEwdwbGdkLWNhMFkwEwYHKoZIzj0CAQYIKoZI\nzj0DAQcDQgAE5s5zak9SL6OCMM49iyJYpNvnHly3UVdTe4jaAxmMS2YM63e68Cix\nevhl849SzCnpoD9yJzFA2HmhEc82IVt66KNjMGEwDgYDVR0PAQH/BAQDAgGGMA8G\nA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCM9S7rizy2ArYhiARButOX7DTPaMB8G\nA1UdIwQYMBaAFELK01Gtmvz99kTvIM5idzOLxerIMAoGCCqGSM49BAMCA0gAMEUC\nIG4Cnqkb+atbFIAeTFyIR4v3ABi0CjPub07xnt/bvN6FAiEAxgyW5sibfw7z3/xc\nQnA0w68sKmVRZhvfUNMYkqbk/2w=\n-----END CERTIFICATE-----\n", + "credentials": [ + { + "type": [ + "ASYMMETRIC_SIGNING_WITH_CERTIFICATE" + ], + "usage": "CERT", + "id": "0", + "subject": "d9dde79c-3916-433b-4789-ca10aad6fc4a", + "publicData": { + "data": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJwRENDQVVtZ0F3SUJBZ0lSQVBKTmRBZ1lFUWhOUGpUeGxGMy8xdjB3Q2dZSUtvWkl6ajBFQXdJd0VqRVEKTUE0R0ExVUVBeE1IY0d4blpDMWpZVEFlRncweU5EQTJNamN3TmpVMU16aGFGdzB5TlRBMU1qVXhOekF5TlRGYQpNRFF4TWpBd0JnTlZCQU1US1hWMWFXUTZaRGxrWkdVM09XTXRNemt4TmkwME16TmlMVFEzT0RrdFkyRXhNR0ZoClpEWm1ZelJoTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFYVdnMFd1cm1ucmw4TFR2QVJIYysKQ3U4ZWlMQklzS1hwL1JBTHFyMmh0c0ZLMGpsa1N0cHV4TVdmU29mYlRpWS84Z0QxWW1NM1E4MzNDTDBTZytDNQpaYU5lTUZ3d0RnWURWUjBQQVFIL0JBUURBZ09JTUNrR0ExVWRKUVFpTUNBR0NDc0dBUVVGQndNQ0JnZ3JCZ0VGCkJRY0RBUVlLS3dZQkJBR0MzbndCQmpBZkJnTlZIU01FR0RBV2dCUWpQVXU2NHM4dGdLMklZZ0VRYnJUbCt3MHoKMmpBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQS9idnQyNnlaSm96SGVYSkd1c3VINEhMWmQ0ZUFrUXk3QmM5eApTbXQzVXVrQ0lRRHg5ZzJEVVBFQ1AvMjBNVjE4RktraXRVQU4zRWQ0dEIvZ21YMTd0Q0J1UXc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlCa0RDQ0FUYWdBd0lCQWdJUUZxL2VyZ3RzM0psYXhrM045MkZmelRBS0JnZ3Foa2pPUFFRREFqQWRNUnN3CkdRWURWUVFERXhKeWIyOTBMbU5oTG5Cc1oyUXVZMnh2ZFdRd0hoY05NalF3TlRJMU1UY3dNalV4V2hjTk1qVXcKTlRJMU1UY3dNalV4V2pBU01SQXdEZ1lEVlFRREV3ZHdiR2RrTFdOaE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSQp6ajBEQVFjRFFnQUU1czV6YWs5U0w2T0NNTTQ5aXlKWXBOdm5IbHkzVVZkVGU0amFBeG1NUzJZTTYzZTY4Q2l4CmV2aGw4NDlTekNucG9EOXlKekZBMkhtaEVjODJJVnQ2NktOak1HRXdEZ1lEVlIwUEFRSC9CQVFEQWdHR01BOEcKQTFVZEV3RUIvd1FGTUFNQkFmOHdIUVlEVlIwT0JCWUVGQ005UzdyaXp5MkFyWWhpQVJCdXRPWDdEVFBhTUI4RwpBMVVkSXdRWU1CYUFGRUxLMDFHdG12ejk5a1R2SU01aWR6T0x4ZXJJTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDCklHNENucWtiK2F0YkZJQWVURnlJUjR2M0FCaTBDalB1YjA3eG50L2J2TjZGQWlFQXhneVc1c2liZnc3ejMveGMKUW5BMHc2OHNLbVZSWmh2ZlVOTVlrcWJrLzJ3PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", + "encoding": "PEM" + } + }, + { + "publicData": { + "data": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJmVENDQVNPZ0F3SUJBZ0lSQUlwR0Jrb0U2ZXdwdFJMT1FaMXZrb1V3Q2dZSUtvWkl6ajBFQXdJd0hURWIKTUJrR0ExVUVBeE1TY205dmRDNWpZUzV3Ykdka0xtTnNiM1ZrTUNBWERUSXpNVEF4T1RBNE5EZ3hOVm9ZRHpJeApNak13T1RJMU1EZzBPREUxV2pBZE1Sc3dHUVlEVlFRREV4SnliMjkwTG1OaExuQnNaMlF1WTJ4dmRXUXdXVEFUCkJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFtMHgrMTZCdWt2ditIV0FlcnkvMG8vVlFBTzhib3hzNHcKT2lhWFRybDBhbi9iUkR6REFwc1J2cDY5SHdJY2haS0FjZTVPQ3JkZFNpVVJOdDZRZWYwQ28wSXdRREFPQmdOVgpIUThCQWY4RUJBTUNBUVl3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVUXNyVFVhMmEvUDMyClJPOGd6bUozTTR2RjZzZ3dDZ1lJS29aSXpqMEVBd0lEU0FBd1JRSWdOMHBnS1N4d2RvUklkV1RxaTdUcDd0eVAKb05LNERxeElsWWJuRUlJb3BlZ0NJUURsd2FUMmg2RUtaM1R3SHFBcTU1UXl3TDM3RzJwdjVKOE03dDFFcVBnQQpZQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + "encoding": "PEM" + }, + "subject": "beb32777-9680-4f42-8761-350eebe76a85", + "type": [ + "ASYMMETRIC_SIGNING_WITH_CERTIFICATE" + ], + "usage": "TRUST_CA", + "id": "0" + } + ] + }, + "acl": { + "accessControlList": [ + { + "deviceSubject": { + "deviceId": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "permissions": [ + "CREATE", + "READ", + "WRITE", + "DELETE", + "NOTIFY" + ], + "resources": [ + { + "href": "/CoapCloudConfResURI", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/sec/sp" + }, + { + "wildcard": "NONE", + "href": "/x.plgd.dev/time", + "interfaces": [ + "*" + ] + }, + { + "href": "/oc/con", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oc/swu", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oic/mnt", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/x.plgd.dev/time" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONCFG_ALL" + } + ] + }, + { + "resources": [ + { + "href": "/oic/d", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/p" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/res" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/sec/sdi" + }, + { + "href": "/oic/sec/doxm", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + } + ], + "connectionSubject": { + "type": "ANON_CLEAR" + }, + "permissions": [ + "READ" + ] + }, + { + "permissions": [ + "CREATE", + "READ", + "WRITE", + "DELETE", + "NOTIFY" + ], + "resources": [ + { + "wildcard": "NONE", + "href": "/oc/con", + "interfaces": [ + "*" + ] + }, + { + "wildcard": "NONE", + "href": "/oc/swu", + "interfaces": [ + "*" + ] + }, + { + "href": "/oic/mnt", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/x.plgd.dev/time", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONCFG_ALL" + } + ], + "deviceSubject": { + "deviceId": "1c10a3b6-287c-11ec-ac2d-13054959c274" + } + } + ], + "status": { + "date": "1719474938340859252", + "coapCode": 69 + } + } + } +} \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/list/list.json b/http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/list/list.json new file mode 100644 index 000000000..b337d0998 --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/dps/provisioning-records/list/list.json @@ -0,0 +1,501 @@ +[ + { + "result": { + "enrollmentGroupId": "4f0d3d81-0e09-4f1e-9706-0997488011af", + "creationDate": "1716652467413672050", + "attestation": { + "date": "1719474938251929779", + "x509": { + "certificatePem": "-----BEGIN CERTIFICATE-----\nMIIBgjCCASmgAwIBAgIQdYzG5cWdt18FWrG0WxBqNzAKBggqhkjOPQQDAjAUMRIw\nEAYDVQQDEwlNZmdSb290Q0EwIBcNMjMxMTA2MDcyNzU5WhgPMjEyMzEwMTMwNzI3\nNTlaMBIxEDAOBgNVBAMTB01mZ0NlcnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\nAAQMkz/uQi1sbMmuxgagH/A5II6CcxZz9sWpSjuxLEqPNEbL021yUaQX6/U2GBYs\npFVWzGPWIog30mgGq/jvDyQKo10wWzAfBgNVHSMEGDAWgBRchhQ5bzFpRdMG/NMg\nyH20R+5UzzAMBgNVHRMEBTADAQEAMAsGA1UdDwQEAwIDiDAdBgNVHSUEFjAUBggr\nBgEFBQcDAgYIKwYBBQUHAwEwCgYIKoZIzj0EAwIDRwAwRAIgbqEU4iOhHpD4YnjC\nO5J8lTIw+RbZ+r9uZD1K2fz4ZfkCIGFzidJQAmBSPVvEdK4X4CSf76BjEoRwXxm2\nKaeI8Vkl\n-----END CERTIFICATE-----\n", + "commonName": "MfgCert" + } + }, + "cloud": { + "status": { + "coapCode": 68, + "date": "1719474938285092512" + }, + "providerName": "plgd.dps", + "gateways": [ + { + "id": "1c10a3b6-287c-11ec-ac2d-13054959c274", + "uri": "coaps+tcp://try.plgd.cloud:5684" + } + ], + "selectedGateway": 0 + }, + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "id": "69f23484-171a-56a9-b300-89cd3007a70c", + "deviceId": "d9dde79c-3916-433b-4789-ca10aad6fc4a", + "credential": { + "status": { + "date": "1719474938324358862", + "coapCode": 68 + }, + "identityCertificatePem": "-----BEGIN CERTIFICATE-----\nMIIBpDCCAUmgAwIBAgIRAPJNdAgYEQhNPjTxlF3/1v0wCgYIKoZIzj0EAwIwEjEQ\nMA4GA1UEAxMHcGxnZC1jYTAeFw0yNDA2MjcwNjU1MzhaFw0yNTA1MjUxNzAyNTFa\nMDQxMjAwBgNVBAMTKXV1aWQ6ZDlkZGU3OWMtMzkxNi00MzNiLTQ3ODktY2ExMGFh\nZDZmYzRhMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaWg0Wurmnrl8LTvARHc+\nCu8eiLBIsKXp/RALqr2htsFK0jlkStpuxMWfSofbTiY/8gD1YmM3Q833CL0Sg+C5\nZaNeMFwwDgYDVR0PAQH/BAQDAgOIMCkGA1UdJQQiMCAGCCsGAQUFBwMCBggrBgEF\nBQcDAQYKKwYBBAGC3nwBBjAfBgNVHSMEGDAWgBQjPUu64s8tgK2IYgEQbrTl+w0z\n2jAKBggqhkjOPQQDAgNJADBGAiEA/bvt26yZJozHeXJGusuH4HLZd4eAkQy7Bc9x\nSmt3UukCIQDx9g2DUPECP/20MV18FKkitUAN3Ed4tB/gmX17tCBuQw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBkDCCATagAwIBAgIQFq/ergts3Jlaxk3N92FfzTAKBggqhkjOPQQDAjAdMRsw\nGQYDVQQDExJyb290LmNhLnBsZ2QuY2xvdWQwHhcNMjQwNTI1MTcwMjUxWhcNMjUw\nNTI1MTcwMjUxWjASMRAwDgYDVQQDEwdwbGdkLWNhMFkwEwYHKoZIzj0CAQYIKoZI\nzj0DAQcDQgAE5s5zak9SL6OCMM49iyJYpNvnHly3UVdTe4jaAxmMS2YM63e68Cix\nevhl849SzCnpoD9yJzFA2HmhEc82IVt66KNjMGEwDgYDVR0PAQH/BAQDAgGGMA8G\nA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCM9S7rizy2ArYhiARButOX7DTPaMB8G\nA1UdIwQYMBaAFELK01Gtmvz99kTvIM5idzOLxerIMAoGCCqGSM49BAMCA0gAMEUC\nIG4Cnqkb+atbFIAeTFyIR4v3ABi0CjPub07xnt/bvN6FAiEAxgyW5sibfw7z3/xc\nQnA0w68sKmVRZhvfUNMYkqbk/2w=\n-----END CERTIFICATE-----\n", + "credentials": [ + { + "type": [ + "ASYMMETRIC_SIGNING_WITH_CERTIFICATE" + ], + "publicData": { + "data": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJwRENDQVVtZ0F3SUJBZ0lSQVBKTmRBZ1lFUWhOUGpUeGxGMy8xdjB3Q2dZSUtvWkl6ajBFQXdJd0VqRVEKTUE0R0ExVUVBeE1IY0d4blpDMWpZVEFlRncweU5EQTJNamN3TmpVMU16aGFGdzB5TlRBMU1qVXhOekF5TlRGYQpNRFF4TWpBd0JnTlZCQU1US1hWMWFXUTZaRGxrWkdVM09XTXRNemt4TmkwME16TmlMVFEzT0RrdFkyRXhNR0ZoClpEWm1ZelJoTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFYVdnMFd1cm1ucmw4TFR2QVJIYysKQ3U4ZWlMQklzS1hwL1JBTHFyMmh0c0ZLMGpsa1N0cHV4TVdmU29mYlRpWS84Z0QxWW1NM1E4MzNDTDBTZytDNQpaYU5lTUZ3d0RnWURWUjBQQVFIL0JBUURBZ09JTUNrR0ExVWRKUVFpTUNBR0NDc0dBUVVGQndNQ0JnZ3JCZ0VGCkJRY0RBUVlLS3dZQkJBR0MzbndCQmpBZkJnTlZIU01FR0RBV2dCUWpQVXU2NHM4dGdLMklZZ0VRYnJUbCt3MHoKMmpBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQS9idnQyNnlaSm96SGVYSkd1c3VINEhMWmQ0ZUFrUXk3QmM5eApTbXQzVXVrQ0lRRHg5ZzJEVVBFQ1AvMjBNVjE4RktraXRVQU4zRWQ0dEIvZ21YMTd0Q0J1UXc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlCa0RDQ0FUYWdBd0lCQWdJUUZxL2VyZ3RzM0psYXhrM045MkZmelRBS0JnZ3Foa2pPUFFRREFqQWRNUnN3CkdRWURWUVFERXhKeWIyOTBMbU5oTG5Cc1oyUXVZMnh2ZFdRd0hoY05NalF3TlRJMU1UY3dNalV4V2hjTk1qVXcKTlRJMU1UY3dNalV4V2pBU01SQXdEZ1lEVlFRREV3ZHdiR2RrTFdOaE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSQp6ajBEQVFjRFFnQUU1czV6YWs5U0w2T0NNTTQ5aXlKWXBOdm5IbHkzVVZkVGU0amFBeG1NUzJZTTYzZTY4Q2l4CmV2aGw4NDlTekNucG9EOXlKekZBMkhtaEVjODJJVnQ2NktOak1HRXdEZ1lEVlIwUEFRSC9CQVFEQWdHR01BOEcKQTFVZEV3RUIvd1FGTUFNQkFmOHdIUVlEVlIwT0JCWUVGQ005UzdyaXp5MkFyWWhpQVJCdXRPWDdEVFBhTUI4RwpBMVVkSXdRWU1CYUFGRUxLMDFHdG12ejk5a1R2SU01aWR6T0x4ZXJJTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDCklHNENucWtiK2F0YkZJQWVURnlJUjR2M0FCaTBDalB1YjA3eG50L2J2TjZGQWlFQXhneVc1c2liZnc3ejMveGMKUW5BMHc2OHNLbVZSWmh2ZlVOTVlrcWJrLzJ3PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", + "encoding": "PEM" + }, + "id": "0", + "subject": "d9dde79c-3916-433b-4789-ca10aad6fc4a", + "usage": "CERT" + }, + { + "subject": "beb32777-9680-4f42-8761-350eebe76a85", + "usage": "TRUST_CA", + "id": "0", + "type": [ + "ASYMMETRIC_SIGNING_WITH_CERTIFICATE" + ], + "publicData": { + "data": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJmVENDQVNPZ0F3SUJBZ0lSQUlwR0Jrb0U2ZXdwdFJMT1FaMXZrb1V3Q2dZSUtvWkl6ajBFQXdJd0hURWIKTUJrR0ExVUVBeE1TY205dmRDNWpZUzV3Ykdka0xtTnNiM1ZrTUNBWERUSXpNVEF4T1RBNE5EZ3hOVm9ZRHpJeApNak13T1RJMU1EZzBPREUxV2pBZE1Sc3dHUVlEVlFRREV4SnliMjkwTG1OaExuQnNaMlF1WTJ4dmRXUXdXVEFUCkJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFtMHgrMTZCdWt2ditIV0FlcnkvMG8vVlFBTzhib3hzNHcKT2lhWFRybDBhbi9iUkR6REFwc1J2cDY5SHdJY2haS0FjZTVPQ3JkZFNpVVJOdDZRZWYwQ28wSXdRREFPQmdOVgpIUThCQWY4RUJBTUNBUVl3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVUXNyVFVhMmEvUDMyClJPOGd6bUozTTR2RjZzZ3dDZ1lJS29aSXpqMEVBd0lEU0FBd1JRSWdOMHBnS1N4d2RvUklkV1RxaTdUcDd0eVAKb05LNERxeElsWWJuRUlJb3BlZ0NJUURsd2FUMmg2RUtaM1R3SHFBcTU1UXl3TDM3RzJwdjVKOE03dDFFcVBnQQpZQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + "encoding": "PEM" + } + } + ] + }, + "acl": { + "status": { + "coapCode": 69, + "date": "1719474938340859252" + }, + "accessControlList": [ + { + "permissions": [ + "CREATE", + "READ", + "WRITE", + "DELETE", + "NOTIFY" + ], + "resources": [ + { + "href": "/CoapCloudConfResURI", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oic/sec/sp", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/x.plgd.dev/time", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oc/con", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oc/swu", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/mnt" + }, + { + "href": "/x.plgd.dev/time", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONCFG_ALL" + } + ], + "deviceSubject": { + "deviceId": "beb32777-9680-4f42-8761-350eebe76a85" + } + }, + { + "connectionSubject": { + "type": "ANON_CLEAR" + }, + "permissions": [ + "READ" + ], + "resources": [ + { + "href": "/oic/d", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/p" + }, + { + "href": "/oic/res", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oic/sec/sdi", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "wildcard": "NONE", + "href": "/oic/sec/doxm", + "interfaces": [ + "*" + ] + } + ] + }, + { + "deviceSubject": { + "deviceId": "1c10a3b6-287c-11ec-ac2d-13054959c274" + }, + "permissions": [ + "CREATE", + "READ", + "WRITE", + "DELETE", + "NOTIFY" + ], + "resources": [ + { + "href": "/oc/con", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oc/swu", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/mnt" + }, + { + "href": "/x.plgd.dev/time", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONCFG_ALL" + } + ] + } + ] + }, + "ownership": { + "status": { + "coapCode": 69, + "date": "1719474938266133666" + }, + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "plgdTime": { + "date": "1719474938192355476", + "coapCode": 69 + }, + "localEndpoints": [ + "coap://10.110.110.12:41885", + "coaps://10.110.110.12:49997", + "coap+tcp://10.110.110.12:35389", + "coaps+tcp://10.110.110.12:33711" + ] + } + },{ + "result": { + "attestation": { + "date": "1717157743336135946", + "x509": { + "commonName": "MfgCert1", + "certificatePem": "-----BEGIN CERTIFICATE-----\nMIIBhTCCASugAwIBAgIRAJ+UC+MPMl6jc9FVeHZEgB4wCgYIKoZIzj0EAwIwFDES\nMBAGA1UEAxMJTWZnUm9vdENBMCAXDTIzMTIxMTEyNTIwMVoYDzIxMjMxMTE3MTI1\nMjAxWjATMREwDwYDVQQDEwhNZmdDZXJ0MTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABFU2BHkqcNsiN68hGoFGzk+QtK78su3HpObu0U6Yhq5tnbGffQ4oTcel4Lhx\nlW5FHWUPI/l2LV9Ej/i3DSaurs6jXTBbMB8GA1UdIwQYMBaAFFyGFDlvMWlF0wb8\n0yDIfbRH7lTPMAwGA1UdEwQFMAMBAQAwCwYDVR0PBAQDAgOIMB0GA1UdJQQWMBQG\nCCsGAQUFBwMCBggrBgEFBQcDATAKBggqhkjOPQQDAgNIADBFAiBM31BMFp/ImqVt\nwH55aaqYSYp00pabSyIbKwFle6B+2wIhAMqXjZ3zoEECNU7zUo/hpYPTqanak2t6\ndPVwtlKcRlPx\n-----END CERTIFICATE-----\n" + } + }, + "cloud": { + "gateways": [ + { + "uri": "coaps+tcp://try.plgd.cloud:5684", + "id": "1c10a3b6-287c-11ec-ac2d-13054959c274" + } + ], + "selectedGateway": 0, + "status": { + "coapCode": 68, + "date": "1717157743426800468" + }, + "providerName": "plgd.dps" + }, + "ownership": { + "status": { + "date": "1717157743352900277", + "coapCode": 69 + }, + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "plgdTime": { + "date": "1717157743283612068", + "coapCode": 69 + }, + "id": "4a0d78b2-9a21-557d-ab08-bb4eaccfcbb0", + "deviceId": "bcd30976-10e4-4145-5282-eb50ee55dfae", + "creationDate": "1716656687662199144", + "localEndpoints": [ + "coap://10.110.110.12:45396", + "coaps://10.110.110.12:52852", + "coap+tcp://10.110.110.12:51863", + "coaps+tcp://10.110.110.12:38145" + ], + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "enrollmentGroupId": "4f0d3d81-0e09-4f1e-9706-0997488011af", + "credential": { + "identityCertificatePem": "-----BEGIN CERTIFICATE-----\nMIIBojCCAUigAwIBAgIQIm1TIO0PH7O+w+LXTfV1KTAKBggqhkjOPQQDAjASMRAw\nDgYDVQQDEwdwbGdkLWNhMB4XDTI0MDUzMTExMTU0M1oXDTI1MDUyNTE3MDI1MVow\nNDEyMDAGA1UEAxMpdXVpZDpiY2QzMDk3Ni0xMGU0LTQxNDUtNTI4Mi1lYjUwZWU1\nNWRmYWUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQK9W+P4MDWdABFVSgZOWnW\nDGEkU8abmg2zVW1MlGPZF4YKpKWEUIoqeVxpm5D/yKtzG15uuvx3U2oU3UbRGL5t\no14wXDAOBgNVHQ8BAf8EBAMCA4gwKQYDVR0lBCIwIAYIKwYBBQUHAwIGCCsGAQUF\nBwMBBgorBgEEAYLefAEGMB8GA1UdIwQYMBaAFCM9S7rizy2ArYhiARButOX7DTPa\nMAoGCCqGSM49BAMCA0gAMEUCIBMdkdXzDBJgThKP3Q1hTV/XzigwlSe7+YlXoINl\ngW8DAiEA7f9ghvAZzBbep1sMDmcTFFLquzAPNsRBSCkSquyYUNQ=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBkDCCATagAwIBAgIQFq/ergts3Jlaxk3N92FfzTAKBggqhkjOPQQDAjAdMRsw\nGQYDVQQDExJyb290LmNhLnBsZ2QuY2xvdWQwHhcNMjQwNTI1MTcwMjUxWhcNMjUw\nNTI1MTcwMjUxWjASMRAwDgYDVQQDEwdwbGdkLWNhMFkwEwYHKoZIzj0CAQYIKoZI\nzj0DAQcDQgAE5s5zak9SL6OCMM49iyJYpNvnHly3UVdTe4jaAxmMS2YM63e68Cix\nevhl849SzCnpoD9yJzFA2HmhEc82IVt66KNjMGEwDgYDVR0PAQH/BAQDAgGGMA8G\nA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCM9S7rizy2ArYhiARButOX7DTPaMB8G\nA1UdIwQYMBaAFELK01Gtmvz99kTvIM5idzOLxerIMAoGCCqGSM49BAMCA0gAMEUC\nIG4Cnqkb+atbFIAeTFyIR4v3ABi0CjPub07xnt/bvN6FAiEAxgyW5sibfw7z3/xc\nQnA0w68sKmVRZhvfUNMYkqbk/2w=\n-----END CERTIFICATE-----\n", + "credentials": [ + { + "publicData": { + "data": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJvakNDQVVpZ0F3SUJBZ0lRSW0xVElPMFBIN08rdytMWFRmVjFLVEFLQmdncWhrak9QUVFEQWpBU01SQXcKRGdZRFZRUURFd2R3Ykdka0xXTmhNQjRYRFRJME1EVXpNVEV4TVRVME0xb1hEVEkxTURVeU5URTNNREkxTVZvdwpOREV5TURBR0ExVUVBeE1wZFhWcFpEcGlZMlF6TURrM05pMHhNR1UwTFRReE5EVXROVEk0TWkxbFlqVXdaV1UxCk5XUm1ZV1V3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFLOVcrUDRNRFdkQUJGVlNnWk9XblcKREdFa1U4YWJtZzJ6VlcxTWxHUFpGNFlLcEtXRVVJb3FlVnhwbTVEL3lLdHpHMTV1dXZ4M1Uyb1UzVWJSR0w1dApvMTR3WERBT0JnTlZIUThCQWY4RUJBTUNBNGd3S1FZRFZSMGxCQ0l3SUFZSUt3WUJCUVVIQXdJR0NDc0dBUVVGCkJ3TUJCZ29yQmdFRUFZTGVmQUVHTUI4R0ExVWRJd1FZTUJhQUZDTTlTN3JpenkyQXJZaGlBUkJ1dE9YN0RUUGEKTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUJNZGtkWHpEQkpnVGhLUDNRMWhUVi9Yemlnd2xTZTcrWWxYb0lObApnVzhEQWlFQTdmOWdodkFaekJiZXAxc01EbWNURkZMcXV6QVBOc1JCU0NrU3F1eVlVTlE9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrRENDQVRhZ0F3SUJBZ0lRRnEvZXJndHMzSmxheGszTjkyRmZ6VEFLQmdncWhrak9QUVFEQWpBZE1Sc3cKR1FZRFZRUURFeEp5YjI5MExtTmhMbkJzWjJRdVkyeHZkV1F3SGhjTk1qUXdOVEkxTVRjd01qVXhXaGNOTWpVdwpOVEkxTVRjd01qVXhXakFTTVJBd0RnWURWUVFERXdkd2JHZGtMV05oTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJCnpqMERBUWNEUWdBRTVzNXphazlTTDZPQ01NNDlpeUpZcE52bkhseTNVVmRUZTRqYUF4bU1TMllNNjNlNjhDaXgKZXZobDg0OVN6Q25wb0Q5eUp6RkEySG1oRWM4MklWdDY2S05qTUdFd0RnWURWUjBQQVFIL0JBUURBZ0dHTUE4RwpBMVVkRXdFQi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZDTTlTN3JpenkyQXJZaGlBUkJ1dE9YN0RUUGFNQjhHCkExVWRJd1FZTUJhQUZFTEswMUd0bXZ6OTlrVHZJTTVpZHpPTHhlcklNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUMKSUc0Q25xa2IrYXRiRklBZVRGeUlSNHYzQUJpMENqUHViMDd4bnQvYnZONkZBaUVBeGd5VzVzaWJmdzd6My94YwpRbkEwdzY4c0ttVlJaaHZmVU5NWWtxYmsvMnc9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + "encoding": "PEM" + }, + "subject": "bcd30976-10e4-4145-5282-eb50ee55dfae", + "usage": "CERT", + "id": "0", + "type": [ + "ASYMMETRIC_SIGNING_WITH_CERTIFICATE" + ] + }, + { + "id": "0", + "type": [ + "ASYMMETRIC_SIGNING_WITH_CERTIFICATE" + ], + "subject": "beb32777-9680-4f42-8761-350eebe76a85", + "usage": "TRUST_CA", + "publicData": { + "data": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJmVENDQVNPZ0F3SUJBZ0lSQUlwR0Jrb0U2ZXdwdFJMT1FaMXZrb1V3Q2dZSUtvWkl6ajBFQXdJd0hURWIKTUJrR0ExVUVBeE1TY205dmRDNWpZUzV3Ykdka0xtTnNiM1ZrTUNBWERUSXpNVEF4T1RBNE5EZ3hOVm9ZRHpJeApNak13T1RJMU1EZzBPREUxV2pBZE1Sc3dHUVlEVlFRREV4SnliMjkwTG1OaExuQnNaMlF1WTJ4dmRXUXdXVEFUCkJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVFtMHgrMTZCdWt2ditIV0FlcnkvMG8vVlFBTzhib3hzNHcKT2lhWFRybDBhbi9iUkR6REFwc1J2cDY5SHdJY2haS0FjZTVPQ3JkZFNpVVJOdDZRZWYwQ28wSXdRREFPQmdOVgpIUThCQWY4RUJBTUNBUVl3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVUXNyVFVhMmEvUDMyClJPOGd6bUozTTR2RjZzZ3dDZ1lJS29aSXpqMEVBd0lEU0FBd1JRSWdOMHBnS1N4d2RvUklkV1RxaTdUcDd0eVAKb05LNERxeElsWWJuRUlJb3BlZ0NJUURsd2FUMmg2RUtaM1R3SHFBcTU1UXl3TDM3RzJwdjVKOE03dDFFcVBnQQpZQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K", + "encoding": "PEM" + } + } + ], + "status": { + "date": "1717157743461436598", + "coapCode": 68 + } + }, + "acl": { + "status": { + "date": "1717157743480774484", + "coapCode": 69 + }, + "accessControlList": [ + { + "permissions": [ + "CREATE", + "READ", + "WRITE", + "DELETE", + "NOTIFY" + ], + "resources": [ + { + "href": "/CoapCloudConfResURI", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oic/sec/sp", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/x.plgd.dev/time", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oc/con", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oc/swu", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/mnt" + }, + { + "wildcard": "NONE", + "href": "/x.plgd.dev/time", + "interfaces": [ + "*" + ] + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONCFG_ALL" + } + ], + "deviceSubject": { + "deviceId": "beb32777-9680-4f42-8761-350eebe76a85" + } + }, + { + "connectionSubject": { + "type": "ANON_CLEAR" + }, + "permissions": [ + "READ" + ], + "resources": [ + { + "href": "/oic/d", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "href": "/oic/p", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/res" + }, + { + "href": "/oic/sec/sdi", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONE", + "href": "/oic/sec/doxm" + } + ] + }, + { + "deviceSubject": { + "deviceId": "1c10a3b6-287c-11ec-ac2d-13054959c274" + }, + "permissions": [ + "CREATE", + "READ", + "WRITE", + "DELETE", + "NOTIFY" + ], + "resources": [ + { + "href": "/oc/con", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "wildcard": "NONE", + "href": "/oc/swu", + "interfaces": [ + "*" + ] + }, + { + "href": "/oic/mnt", + "interfaces": [ + "*" + ], + "wildcard": "NONE" + }, + { + "wildcard": "NONE", + "href": "/x.plgd.dev/time", + "interfaces": [ + "*" + ] + }, + { + "interfaces": [ + "*" + ], + "wildcard": "NONCFG_ALL" + } + ] + } + ] + } + } +} +] \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/data/pending-commands.json b/http-gateway/web/packages/mock-server/src/data/pending-commands.json index 0ab710b6d..f827e419f 100644 --- a/http-gateway/web/packages/mock-server/src/data/pending-commands.json +++ b/http-gateway/web/packages/mock-server/src/data/pending-commands.json @@ -1,50 +1,82 @@ [ { - "resourceUpdatePending": { - "resourceId": { - "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", - "href": "/light/1" - }, - "content": { - "state": false, - "power": 1, - "powera": 1, - "powerb": 1, - "powerc": 1, - "powerd": 1, - "powere": 1, - "powerf": 1, - "powerg": 1, - "powerh": 1, - "poweri": 1, - "powerj": 1, - "powerk": 1, - "powerl": 1, - "powerm": 1, - "powern": 1, - "powero": 1, - "powerp": 1, - "powerq": 1, - "powerr": 1, - "name": "Light" - }, - "auditContext": { - "correlationId": "a3e05bc0-205a-48b1-92ed-78ea0b23e78f", - "userId": "98120ad9-eefa-4b93-aa53-696420b639fd" - }, - "eventMetadata": { - "connectionId": "10.1.65.233:38320", - "sequence": "0", - "version": "4", - "timestamp": "1679950925732370668" - }, - "validUntil": "1679950935732388581", - "openTelemetryCarrier": { - "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" + "result": { + "resourceUpdatePending": { + "resourceId": { + "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", + "href": "/light/1" + }, + "content": { + "state": false, + "power": 1, + "powera": 1, + "powerb": 1, + "powerc": 1, + "powerd": 1, + "powere": 1, + "powerf": 1, + "powerg": 1, + "powerh": 1, + "poweri": 1, + "powerj": 1, + "powerk": 1, + "powerl": 1, + "powerm": 1, + "powern": 1, + "powero": 1, + "powerp": 1, + "powerq": 1, + "powerr": 1, + "name": "Light" + }, + "auditContext": { + "correlationId": "a3e05bc0-205a-48b1-92ed-78ea0b23e78f", + "userId": "98120ad9-eefa-4b93-aa53-696420b639fd" + }, + "eventMetadata": { + "connectionId": "10.1.65.233:38320", + "sequence": "0", + "version": "4", + "timestamp": "1679950925732370668" + }, + "validUntil": "1679950935732388581", + "openTelemetryCarrier": { + "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" + } + } + } +}, +{ + "result": { + "resourceUpdatePending": { + "resourceId": { + "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", + "href": "/light/1" + }, + "content": { + "state": false, + "power": 1, + "name": "Light" + }, + "auditContext": { + "correlationId": "a3e05bc0-205a-48b1-92ed-78ea0b23e78f", + "userId": "98120ad9-eefa-4b93-aa53-696420b639fd" + }, + "eventMetadata": { + "connectionId": "10.1.65.233:38320", + "sequence": "0", + "version": "4", + "timestamp": "1679950925732370668" + }, + "validUntil": "1679950935732388581", + "openTelemetryCarrier": { + "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" + } } } - }, - { +}, +{ + "result": { "resourceUpdatePending": { "resourceId": { "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", @@ -69,9 +101,11 @@ "openTelemetryCarrier": { "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" } - } - }, - { + } +} +}, +{ + "result": { "resourceUpdatePending": { "resourceId": { "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", @@ -97,8 +131,10 @@ "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" } } - }, - { +} +}, +{ + "result": { "resourceUpdatePending": { "resourceId": { "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", @@ -124,8 +160,10 @@ "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" } } - }, - { + } +}, +{ + "result": { "resourceUpdatePending": { "resourceId": { "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", @@ -151,8 +189,10 @@ "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" } } - }, - { + } +}, +{ + "result": { "resourceUpdatePending": { "resourceId": { "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", @@ -178,8 +218,10 @@ "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" } } - }, - { + } +}, +{ + "result": { "resourceUpdatePending": { "resourceId": { "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", @@ -205,8 +247,10 @@ "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" } } - }, - { +} +}, +{ + "result": { "resourceUpdatePending": { "resourceId": { "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", @@ -232,8 +276,10 @@ "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" } } - }, - { +} +}, +{ + "result": { "resourceUpdatePending": { "resourceId": { "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", @@ -259,8 +305,10 @@ "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" } } - }, - { +} +}, +{ + "result": { "resourceUpdatePending": { "resourceId": { "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", @@ -286,8 +334,10 @@ "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" } } - }, - { +} +}, +{ + "result": { "resourceUpdatePending": { "resourceId": { "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", @@ -313,8 +363,10 @@ "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" } } - }, - { +} +}, +{ + "result": { "resourceUpdatePending": { "resourceId": { "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", @@ -340,59 +392,59 @@ "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" } } - }, + } +}, { - "resourceUpdatePending": { - "resourceId": { - "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", - "href": "/light/1" - }, - "content": { - "state": false, - "power": 1, - "name": "Light" - }, - "auditContext": { - "correlationId": "a3e05bc0-205a-48b1-92ed-78ea0b23e78f", - "userId": "98120ad9-eefa-4b93-aa53-696420b639fd" - }, - "eventMetadata": { - "connectionId": "10.1.65.233:38320", - "sequence": "0", - "version": "4", - "timestamp": "1679950925732370668" - }, - "validUntil": "1679950935732388581", - "openTelemetryCarrier": { - "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" + "result": { + "resourceUpdatePending": { + "resourceId": { + "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", + "href": "/light/1" + }, + "content": { + "state": false, + "power": 1, + "name": "Light" + }, + "auditContext": { + "correlationId": "a3e05bc0-205a-48b1-92ed-78ea0b23e78f", + "userId": "98120ad9-eefa-4b93-aa53-696420b639fd" + }, + "eventMetadata": { + "connectionId": "10.1.65.233:38320", + "sequence": "0", + "version": "4", + "timestamp": "1679950925732370668" + }, + "validUntil": "1679950935732388581", + "openTelemetryCarrier": { + "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" + } } } }, { - "resourceUpdatePending": { - "resourceId": { - "deviceId": "9c387d90-6ca4-4473-4b6c-f4fb2f8ad0e9", - "href": "/light/1" - }, - "content": { - "state": false, - "power": 1, - "name": "Light" - }, - "auditContext": { - "correlationId": "a3e05bc0-205a-48b1-92ed-78ea0b23e78f", - "userId": "98120ad9-eefa-4b93-aa53-696420b639fd" - }, - "eventMetadata": { - "connectionId": "10.1.65.233:38320", - "sequence": "0", - "version": "4", - "timestamp": "1679950925732370668" - }, - "validUntil": "1679950935732388581", - "openTelemetryCarrier": { - "traceparent": "00-8177df6ddb079f50d1eef17eb893b048-992be580d42978b1-01" + "result": { + "resourceUpdatePending": { + "eventMetadata": { + "version": "2", + "timestamp": "1718962348706048929", + "connectionId": "48998f7d-2a70-46a4-8a68-745b69d55489", + "sequence": "0", + "hubId": "1c10a3b6-287c-11ec-ac2d-13054959c274" + }, + "validUntil": "1719480748706050211", + "resourceId": { + "deviceId": "bcd30976-10e4-4145-5282-eb50ee55dfae", + "href": "/not/existing" + }, + "content": 123456, + "auditContext": { + "correlationId": "b3e46873-228b-403e-ad91-b5f97be3b3d7", + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "userId": "beb32777-9680-4f42-8761-350eebe76a85" + } } } } -] \ No newline at end of file +] diff --git a/http-gateway/web/packages/mock-server/src/data/snippet-service/applied-configurations/list/httpConfigurationIdFilter-48998f7d-2a70-46a4-8a68-745b69d55489.json b/http-gateway/web/packages/mock-server/src/data/snippet-service/applied-configurations/list/httpConfigurationIdFilter-48998f7d-2a70-46a4-8a68-745b69d55489.json new file mode 100644 index 000000000..f56e8a539 --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/snippet-service/applied-configurations/list/httpConfigurationIdFilter-48998f7d-2a70-46a4-8a68-745b69d55489.json @@ -0,0 +1,272 @@ +[ + { + "result": { + "resources": [ + { + "resourceUpdated": { + "auditContext": { + "correlationId": "8d140cad-62ac-4fff-aa8f-48f413ca85ca.412a4dd7-3c9b-4dcb-9696-1f0eb6918a6d", + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "eventMetadata": { + "version": "0", + "timestamp": "0", + "connectionId": "48998f7d-2a70-46a4-8a68-745b69d55489", + "sequence": "0" + }, + "resourceId": { + "deviceId": "7b645eab-01c3-483a-6289-98fc24e32bc1", + "href": "/not/existing" + }, + "status": "ERROR", + "content": "pending resource update canceled" + }, + "validUntil": "0", + "href": "/not/existing", + "correlationId": "8d140cad-62ac-4fff-aa8f-48f413ca85ca.412a4dd7-3c9b-4dcb-9696-1f0eb6918a6d", + "status": "DONE" + }, + { + "status": "DONE", + "resourceUpdated": { + "resourceId": { + "deviceId": "7b645eab-01c3-483a-6289-98fc24e32bc1", + "href": "/oc/con" + }, + "status": "CANCELED", + "auditContext": { + "userId": "beb32777-9680-4f42-8761-350eebe76a85", + "correlationId": "8d140cad-62ac-4fff-aa8f-48f413ca85ca.55d8f61c-f85b-4a0c-aaba-c77f237600dd", + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "eventMetadata": { + "version": "2", + "timestamp": "1719582951907143230", + "connectionId": "10.1.65.223:46696", + "sequence": "0", + "hubId": "1c10a3b6-287c-11ec-ac2d-13054959c274" + }, + "resourceTypes": [ + "oic.wk.con" + ] + }, + "validUntil": "0", + "href": "/oc/con", + "correlationId": "8d140cad-62ac-4fff-aa8f-48f413ca85ca.55d8f61c-f85b-4a0c-aaba-c77f237600dd" + } + ], + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "timestamp": "1719817002027697453", + "id": "8d140cad-62ac-4fff-aa8f-48f413ca85ca", + "deviceId": "7b645eab-01c3-483a-6289-98fc24e32bc1", + "configurationId": { + "id": "48998f7d-2a70-46a4-8a68-745b69d55489", + "version": "0" + }, + "onDemand": true + } + }, + { + "result": { + "onDemand": true, + "resources": [ + { + "href": "/not/existing", + "correlationId": "0f9dc817-333f-4556-9888-c01ed2676478.dd79c0ab-004f-43e3-bc03-58c8c44c6fcd", + "status": "DONE", + "resourceUpdated": { + "status": "ERROR", + "content": "pending resource update canceled", + "auditContext": { + "correlationId": "0f9dc817-333f-4556-9888-c01ed2676478.dd79c0ab-004f-43e3-bc03-58c8c44c6fcd", + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "eventMetadata": { + "connectionId": "48998f7d-2a70-46a4-8a68-745b69d55489", + "sequence": "0", + "version": "0", + "timestamp": "0" + }, + "resourceId": { + "deviceId": "bcd30976-10e4-4145-5282-eb50ee55dfae", + "href": "/not/existing" + } + }, + "validUntil": "0" + }, + { + "href": "/oc/con", + "correlationId": "0f9dc817-333f-4556-9888-c01ed2676478.75be0ddc-e1e0-4738-9a88-b704ae0fc248", + "status": "DONE", + "resourceUpdated": { + "resourceTypes": [ + "oic.wk.con" + ], + "resourceId": { + "deviceId": "bcd30976-10e4-4145-5282-eb50ee55dfae", + "href": "/oc/con" + }, + "status": "OK", + "content": { + "n": "prezentacia dankovi" + }, + "auditContext": { + "userId": "1e3a67e3-ec52-42e2-8bb0-aae61d4af7c8", + "correlationId": "0f9dc817-333f-4556-9888-c01ed2676478.75be0ddc-e1e0-4738-9a88-b704ae0fc248", + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "eventMetadata": { + "version": "281", + "timestamp": "1719595063306772686", + "connectionId": "10.110.110.10:30226", + "sequence": "10159", + "hubId": "1c10a3b6-287c-11ec-ac2d-13054959c274" + } + }, + "validUntil": "0" + } + ], + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "timestamp": "1719817002006350403", + "id": "0f9dc817-333f-4556-9888-c01ed2676478", + "deviceId": "bcd30976-10e4-4145-5282-eb50ee55dfae", + "configurationId": { + "id": "48998f7d-2a70-46a4-8a68-745b69d55489", + "version": "0" + } + } +}, + { + "result": { + "deviceId": "d9dde79c-3916-433b-4789-ca10aad6fc4a", + "configurationId": { + "id": "48998f7d-2a70-46a4-8a68-745b69d55489", + "version": "0" + }, + "onDemand": true, + "resources": [ + { + "status": "DONE", + "resourceUpdated": { + "resourceId": { + "deviceId": "d9dde79c-3916-433b-4789-ca10aad6fc4a", + "href": "/not/existing" + }, + "status": "ERROR", + "content": "pending resource update canceled", + "auditContext": { + "correlationId": "9dae740f-a598-4411-8cd0-cbf7c804426a.a0c76318-6f04-4c04-9c98-c0f87a0e8a1f", + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "eventMetadata": { + "connectionId": "48998f7d-2a70-46a4-8a68-745b69d55489", + "sequence": "0", + "version": "0", + "timestamp": "0" + } + }, + "validUntil": "0", + "href": "/not/existing", + "correlationId": "9dae740f-a598-4411-8cd0-cbf7c804426a.a0c76318-6f04-4c04-9c98-c0f87a0e8a1f" + }, + { + "status": "DONE", + "resourceUpdated": { + "resourceId": { + "deviceId": "d9dde79c-3916-433b-4789-ca10aad6fc4a", + "href": "/oc/con" + }, + "status": "OK", + "content": { + "n": "prezentacia dankovi" + }, + "auditContext": { + "correlationId": "9dae740f-a598-4411-8cd0-cbf7c804426a.38e0d0de-4b86-468f-8a61-e7e5df3edf98", + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "userId": "1e3a67e3-ec52-42e2-8bb0-aae61d4af7c8" + }, + "eventMetadata": { + "version": "5", + "timestamp": "1719595063306023868", + "connectionId": "10.110.110.10:56076", + "sequence": "10153", + "hubId": "1c10a3b6-287c-11ec-ac2d-13054959c274" + }, + "resourceTypes": [ + "oic.wk.con" + ] + }, + "validUntil": "0", + "href": "/oc/con", + "correlationId": "9dae740f-a598-4411-8cd0-cbf7c804426a.38e0d0de-4b86-468f-8a61-e7e5df3edf98" + } + ], + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "timestamp": "1719817002023535166", + "id": "9dae740f-a598-4411-8cd0-cbf7c804426a" + } +}, + { + "result": { + "timestamp": "1719595103190656748", + "id": "40cab4b8-e9d6-47d7-bd70-4daa85c094ad", + "deviceId": "wdsw", + "configurationId": { + "id": "48998f7d-2a70-46a4-8a68-745b69d55489", + "version": "0" + }, + "onDemand": true, + "resources": [ + { + "validUntil": "0", + "href": "/not/existing", + "correlationId": "40cab4b8-e9d6-47d7-bd70-4daa85c094ad.18830e29-5204-4fc9-a76f-91ab65409431", + "status": "DONE", + "resourceUpdated": { + "status": "ERROR", + "content": "rpc error: code = PermissionDenied desc = cannot validate user access: rpc error: code = PermissionDenied desc = access denied", + "auditContext": { + "correlationId": "40cab4b8-e9d6-47d7-bd70-4daa85c094ad.18830e29-5204-4fc9-a76f-91ab65409431", + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "eventMetadata": { + "version": "0", + "timestamp": "0", + "connectionId": "48998f7d-2a70-46a4-8a68-745b69d55489", + "sequence": "0" + }, + "resourceId": { + "deviceId": "wdsw", + "href": "/not/existing" + } + } + }, + { + "href": "/oc/con", + "correlationId": "40cab4b8-e9d6-47d7-bd70-4daa85c094ad.d4de7422-8543-4a67-bb25-4f41e1fc7e6d", + "status": "DONE", + "resourceUpdated": { + "resourceId": { + "deviceId": "wdsw", + "href": "/oc/con" + }, + "status": "ERROR", + "content": "rpc error: code = PermissionDenied desc = cannot validate user access: rpc error: code = PermissionDenied desc = access denied", + "auditContext": { + "correlationId": "40cab4b8-e9d6-47d7-bd70-4daa85c094ad.d4de7422-8543-4a67-bb25-4f41e1fc7e6d", + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + }, + "eventMetadata": { + "sequence": "0", + "version": "0", + "timestamp": "0", + "connectionId": "48998f7d-2a70-46a4-8a68-745b69d55489" + } + }, + "validUntil": "0" + } + ], + "owner": "beb32777-9680-4f42-8761-350eebe76a85" + } +} +] \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/data/snippet-service/conditions/list/list.json b/http-gateway/web/packages/mock-server/src/data/snippet-service/conditions/list/list.json new file mode 100644 index 000000000..2667ea980 --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/snippet-service/conditions/list/list.json @@ -0,0 +1,18 @@ +[ + { + "result": { + "id": "00fa41ad-b3bf-4f00-bfe1-c71c439e4cda", + "enabled": true, + "jqExpressionFilter": ".n == \"new name value\"", + "version": "6", + "name": "jkralik-cond-0", + "configurationId": "48998f7d-2a70-46a4-8a68-745b69d55489", + "resourceHrefFilter": [ + "/oic/d" + ], + "apiAccessToken": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJRQkp3R192eHhVSUNXVjFlZjRlOHlFamgtVHZmYW1FWFBMZGt4WGtENU1jIn0.eyJleHAiOjE3MTk1MDE1MTcsImlhdCI6MTcxOTQ5NDMxNywiYXV0aF90aW1lIjoxNzE5NDc0NzA3LCJqdGkiOiJhM2UyYjU2OC0zYWY4LTQ2ZDMtOGJlOC1jMDMyYWU5NDM0YTgiLCJpc3MiOiJodHRwczovL2F1dGgucGxnZC5jbG91ZC9yZWFsbXMvc2hhcmVkIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImJlYjMyNzc3LTk2ODAtNGY0Mi04NzYxLTM1MGVlYmU3NmE4NSIsInR5cCI6IkJlYXJlciIsImF6cCI6IkxYWjlPaEtXV1JZcWYxMlcwQjVPWGR1cXQwMnEwempTIiwic2Vzc2lvbl9zdGF0ZSI6ImRlYzBhNjk5LWE2NWYtNDcxYy05MGMyLWNlZTAwM2VmMDIxMCIsImFjciI6IjAiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovLzEyNy4wLjAuMTozMDAwIiwiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiaHR0cHM6Ly90cnkucGxnZC5jbG91ZCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1zaGFyZWQiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsInNpZCI6ImRlYzBhNjk5LWE2NWYtNDcxYy05MGMyLWNlZTAwM2VmMDIxMCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJvd25lci1pZCI6ImJlYjMyNzc3LTk2ODAtNGY0Mi04NzYxLTM1MGVlYmU3NmE4NSIsIm5hbWUiOiJUZXN0IFRlc3QiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0ZXN0LnVzZXIub2NmY2xvdWRAZ21haWwuY29tIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlRlc3QiLCJlbWFpbCI6InRlc3QudXNlci5vY2ZjbG91ZEBnbWFpbC5jb20ifQ.aL52J-JuGuW_2R2x33pAVUX3fxJzPcz7OUeqvrPaijOqikOiQSwDyVdt0QIJtD2WBq0StTI-YqSHpt01Kpy1CgUricVWXoCI1j9eRBgveQ7zwiWKqNa1-ctXOHmDugVwSuhYoNBUP94qK1yrTFxSU5Lamj8l7H6pN4KVOMhs48Xbn-EJ7DPbJJ7Z26fJaYwj2P-e_ucV5JrIDPzSGRCBsXpqGlm6w2E2Z-SbwRclU4Xvf7V44bIJLFOV8wS0PrIz030n41F52c0dF1CC-ebnRQsFih6unR0l6J1P2sVoM4EKnR-mVIzfxmCxpXgduIaQU-jVo420vMrgCmluc23vnw", + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "timestamp": "1719494607886702017" + } + } +] \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/data/snippet-service/configurations/detail/48998f7d-2a70-46a4-8a68-745b69d55489.json b/http-gateway/web/packages/mock-server/src/data/snippet-service/configurations/detail/48998f7d-2a70-46a4-8a68-745b69d55489.json new file mode 100644 index 000000000..26e548bd8 --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/snippet-service/configurations/detail/48998f7d-2a70-46a4-8a68-745b69d55489.json @@ -0,0 +1,50 @@ +[ + { + "result": { + "id": "48998f7d-2a70-46a4-8a68-745b69d55489", + "version": "0", + "name": "my-cfg-1", + "resources": [ + { + "href": "/not/existing", + "content": 123456, + "timeToLive": "0" + }, + { + "timeToLive": "0", + "href": "/oc/con", + "content": { + "n": "prezentacia dankovi" + } + } + ], + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "timestamp": "1718961203704580832" + } + }, + { + "result": { + "id": "48998f7d-2a70-46a4-8a68-745b69d55489", + "version": "1", + "name": "my-cfg-11", + "resources": [ + { + "href": "/not/existing", + "content": 123456, + "timeToLive": "0" + }, + { + "href": "/oc/con", + "content": { + "data": "W29iamVjdCBPYmplY3Rd", + "contentType": "application/json", + "coapContentFormat": -1 + }, + "timeToLive": "0" + } + ], + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "timestamp": "1719739387326389953" + } +} +] \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/data/snippet-service/configurations/list/list.json b/http-gateway/web/packages/mock-server/src/data/snippet-service/configurations/list/list.json new file mode 100644 index 000000000..a941dd07c --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/data/snippet-service/configurations/list/list.json @@ -0,0 +1,25 @@ +[ + { + "result": { + "id": "48998f7d-2a70-46a4-8a68-745b69d55489", + "version": "1", + "name": "my-cfg-1", + "resources": [ + { + "href": "/not/existing", + "content": 123456, + "timeToLive": "0" + }, + { + "href": "/oc/con", + "content": { + "n": "prezentacia dankovi" + }, + "timeToLive": "0" + } + ], + "owner": "beb32777-9680-4f42-8761-350eebe76a85", + "timestamp": "1718961203704580832" + } + } +] \ No newline at end of file diff --git a/http-gateway/web/packages/mock-server/src/index.js b/http-gateway/web/packages/mock-server/src/index.js index b03978aa2..45f303a15 100644 --- a/http-gateway/web/packages/mock-server/src/index.js +++ b/http-gateway/web/packages/mock-server/src/index.js @@ -1,25 +1,15 @@ const express = require('express') -const { check, validationResult } = require('express-validator') const cors = require('cors') -const path = require('path') +const axios = require('axios') +const { checkError, loadResponseStreamFromFile } = require('./utils') + +const devices = require('./routes/devices') +const dps = require('./routes/dps') +const snippetService = require('./routes/snippet-service') const app = express() const port = 8181 -let deletedDevice = false -let resourceColorUpdatedValue = false - -const deviceIdCheck = [check('deviceId').notEmpty().withMessage('Device ID must be alphanumeric')] - -const checkError = (req, res) => { - const errors = validationResult(req) - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }) - } - - console.log(`${req.method}`, req.url) -} - app.use( cors({ origin: '*', @@ -27,116 +17,46 @@ app.use( }) ) -const loadResponseFromFile = (file, res) => { - const targetDirectory = `${__dirname}/data` - - res.sendFile(file, { root: targetDirectory }) -} - -// ----- DEVICES ----- -app.get('/api/v1/devices', function (req, res) { - console.log(`${req.method}`, req.url) - - if (deletedDevice) { - loadResponseFromFile(path.join('devices', 'list', 'list-deleted-state.json'), res) - } else { - loadResponseFromFile(path.join('devices', 'list', 'list.json'), res) - } -}) - -app.delete('/api/v1/devices', function (req, res) { +// ----- PENDING COMMANDS ----- +app.get('/api/v1/pending-commands', function (req, res) { console.log(`${req.method}`, req.url) - deletedDevice = true - res.send() -}) - -app.get('/api/v1/devices/:deviceId', deviceIdCheck, function (req, res) { - checkError(req, res) - loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}.json`), res) -}) - -app.get('/api/v1/devices/:deviceId/pending-commands', deviceIdCheck, function (req, res) { - checkError(req, res) - res.send() -}) - -app.get('/api/v1/devices/:deviceId/resources', deviceIdCheck, function (req, res) { - checkError(req, res) - res.send() -}) - -app.put('/api/v1/devices/:deviceId/metadata', deviceIdCheck, function (req, res) { - checkError(req, res) - res.send() -}) - -// change device name -app.put('/api/v1/devices/:deviceId/resources/oc/con', deviceIdCheck, function (req, res) { - checkError(req, res) - res.send({ n: 'New Device Name' }) -}) - -// resource detail -app.get('/api/v1/devices/:deviceId/resources/light/1', deviceIdCheck, function (req, res) { - checkError(req, res) - loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-light-1.json`), res) + loadResponseStreamFromFile('pending-commands.json', res) }) -// resource detail -app.get('/api/v1/devices/:deviceId/resources/.well-known/wot', deviceIdCheck, function (req, res) { - checkError(req, res) - loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-well-known-wot.json`), res) +app.get('/', () => { + console.log(`HUB API mock server listening on port ${port}`) }) -app.get('/api/v1/devices/:deviceId/resources/color', deviceIdCheck, function (req, res) { - checkError(req, res) - - if (resourceColorUpdatedValue) { - loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-color-update.json`), res) - } else { - loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-color.json`), res) +app.get('/.well-known/configuration', (req, res) => { + try { + checkError(req, res) + axios.get('https://try.plgd.cloud/.well-known/configuration').then((r) => res.send(r.data)) + } catch (e) { + res.status(500).send(e.toString()) } }) -app.put('/api/v1/devices/:deviceId/resources/color', deviceIdCheck, function (req, res) { - checkError(req, res) - resourceColorUpdatedValue = true - loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-color-update.json`), res) -}) - -// resource detail update -app.put('/api/v1/devices/:deviceId/resources/light/1', deviceIdCheck, function (req, res) { - checkError(req, res) - loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-light-1.json`), res) -}) - -// ----- GENERAL for devices ----- -app.get('/api/v1/resource-links', function (req, res) { - console.log(`${req.method}`, req.url) - loadResponseFromFile(path.join('devices', 'detail', `${req.query['device_id_filter']}-resource-links.json`), res) -}) - -app.get('/api/v1/provisioning-records', function (req, res) { - console.log(`${req.method}`, req.url) - loadResponseFromFile(path.join('devices', 'detail', `${req.query['deviceIdFilter']}-provisioning-records.json`), res) -}) - -app.get('/api/v1/signing/records', function (req, res) { - console.log(`${req.method}`, req.url) - loadResponseFromFile(path.join('devices', 'detail', `${req.query['deviceIdFilter']}-signin-records.json`), res) +app.get('/theme/theme.json', (req, res) => { + try { + checkError(req, res) + axios.get('https://try.plgd.cloud/theme/theme.json').then((r) => res.send(r.data)) + } catch (e) { + res.status(500).send(e.toString()) + } }) -// ----- PENDING COMMANDS ----- -app.get('/api/v1/pending-commands', function (req, res) { - console.log(`${req.method}`, req.url) - loadResponseFromFile('pending-commands.json', res) +app.get('/repos/plgd-dev/hub/releases/latest', (req, res) => { + try { + checkError(req, res) + axios.get('https://api.github.com/repos/plgd-dev/hub/releases/latest').then((r) => res.send(r.data)) + } catch (e) { + res.status(500).send(e.toString()) + } }) -app.get('/', () => { - console.log(`HUB API mock server listening on port ${port}`) - deletedDevice = false - resourceColorUpdatedValue = false -}) +app.use(devices) +app.use(dps) +app.use('/snippet-service', snippetService) app.listen(port, () => { console.log(`HUB API mock server listening on port ${port}`) diff --git a/http-gateway/web/packages/mock-server/src/routes/devices.js b/http-gateway/web/packages/mock-server/src/routes/devices.js new file mode 100644 index 000000000..f16a7e2e4 --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/routes/devices.js @@ -0,0 +1,181 @@ +const express = require('express') +const { checkError, loadResponseFromFile } = require('../utils') +const path = require('path') +const { check } = require('express-validator') +const escapeHtml = require('escape-html') + +const router = express.Router() + +let deletedDevice = false +let resourceColorUpdatedValue = false + +const deviceIdCheck = [check('deviceId').notEmpty().withMessage('Device ID must be alphanumeric')] + +router.get('/api/v1/devices/api-reset', (req, res) => { + try { + checkError(req, res) + + deletedDevice = false + resourceColorUpdatedValue = false + + res.send('OK') + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +router.get('/api/v1/devices', (req, res) => { + try { + checkError(req, res) + + if (deletedDevice) { + loadResponseFromFile(path.join('devices', 'list', 'list-deleted-state.json'), res) + } else { + loadResponseFromFile(path.join('devices', 'list', 'list.json'), res) + } + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +router.delete('/api/v1/devices', (req, res) => { + try { + checkError(req, res) + deletedDevice = true + + res.send('OK') + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +router.get('/api/v1/devices/:deviceId', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}.json`), res) + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +router.get('/api/v1/devices/:deviceId/pending-commands', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + res.send() + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +router.get('/api/v1/devices/:deviceId/resources', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + res.send() + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +router.put('/api/v1/devices/:deviceId/metadata', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + res.send() + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +// change device name +router.put('/api/v1/devices/:deviceId/resources/oc/con', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + res.send({ n: 'New Device Name' }) + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +// resource detail +router.get('/api/v1/devices/:deviceId/resources/light/1', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-light-1.json`), res) + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +// resource detail update +router.put('/api/v1/devices/:deviceId/resources/light/1', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-light-1.json`), res) + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +// resource detail wot +router.get('/api/v1/devices/:deviceId/resources/.well-known/wot', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-well-known-wot.json`), res) + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +// resource detail color +router.get('/api/v1/devices/:deviceId/resources/color', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + + if (resourceColorUpdatedValue) { + loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-color-update.json`), res) + } else { + loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-color.json`), res) + } + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +// resource detail color update +router.put('/api/v1/devices/:deviceId/resources/color', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + resourceColorUpdatedValue = true + loadResponseFromFile(path.join('devices', 'detail', `${req.params['deviceId']}-resources-color-update.json`), res) + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +router.get('/api/v1/resource-links', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + loadResponseFromFile(path.join('devices', 'detail', `${req.query['device_id_filter']}-resource-links.json`), res) + } catch (e) { + res.status(500).send(e.toString()) + } +}) + +// router.get('/api/v1/provisioning-records', deviceIdCheck, (req, res) => { +// try { +// checkError(req, res) +// loadResponseFromFile(path.join('devices', 'detail', `${req.query['deviceIdFilter']}-provisioning-records.json`), res) +// } catch (e) { +// res.status(500).send(escapeHtml(e.toString())) +// } +// }) + +router.get('/api/v1/signing/records', deviceIdCheck, (req, res) => { + try { + checkError(req, res) + loadResponseFromFile(path.join('devices', 'detail', `${req.query['deviceIdFilter']}-signin-records.json`), res) + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +module.exports = router diff --git a/http-gateway/web/packages/mock-server/src/routes/dps.js b/http-gateway/web/packages/mock-server/src/routes/dps.js new file mode 100644 index 000000000..059e1087e --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/routes/dps.js @@ -0,0 +1,57 @@ +const { checkError, loadResponseStreamFromFile, loadResponseFromFile } = require('../utils') +const get = require('lodash/get') +const path = require('path') +const escapeHtml = require('escape-html') +const express = require('express') + +const router = express.Router() + +router.get('/api/v1/hubs', (req, res) => { + try { + checkError(req, res) + const httpIdFilter = get(req.query, 'idFilter', null) + + // detail configuration page + if (httpIdFilter) { + loadResponseFromFile(path.join('dps', 'linked-hubs', 'detail', `${httpIdFilter}.json`), res) + } else { + loadResponseStreamFromFile(path.join('dps', 'linked-hubs', 'list', `list.json`), res) + } + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +router.get('/api/v1/enrollment-groups', (req, res) => { + try { + checkError(req, res) + const httpIdFilter = get(req.query, 'idFilter', null) + + // detail configuration page + if (httpIdFilter) { + loadResponseFromFile(path.join('dps', 'enrollment-groups', 'detail', `${httpIdFilter}.json`), res) + } else { + loadResponseStreamFromFile(path.join('dps', 'enrollment-groups', 'list', `list.json`), res) + } + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +router.get('/api/v1/provisioning-records', (req, res) => { + try { + checkError(req, res) + const httpIdFilter = get(req.query, 'idFilter', null) + + // detail configuration page + if (httpIdFilter) { + loadResponseFromFile(path.join('dps', 'provisioning-records', 'detail', `${httpIdFilter}.json`), res) + } else { + loadResponseStreamFromFile(path.join('dps', 'provisioning-records', 'list', `list.json`), res) + } + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +module.exports = router diff --git a/http-gateway/web/packages/mock-server/src/routes/snippet-service.js b/http-gateway/web/packages/mock-server/src/routes/snippet-service.js new file mode 100644 index 000000000..75386803a --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/routes/snippet-service.js @@ -0,0 +1,62 @@ +const express = require('express') +const { loadResponseStreamFromFile, checkError } = require('../utils') +const path = require('path') +const escapeHtml = require('escape-html') +const get = require('lodash/get') + +const router = express.Router() + +router.get('/api/v1/configurations/applied', (req, res) => { + try { + checkError(req, res) + const httpConfigurationIdFilter = get(req.query, 'httpConfigurationIdFilter', null)?.replace('/all', '') + + // detail configuration page + if (httpConfigurationIdFilter) { + loadResponseStreamFromFile( + path.join('snippet-service', 'applied-configurations', 'list', `httpConfigurationIdFilter-${httpConfigurationIdFilter}.json`), + res + ) + } else { + // res.send([]) + } + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +router.get('/api/v1/configurations', (req, res) => { + try { + checkError(req, res) + const filter = get(req.query, 'httpIdFilter', null)?.replace('/all', '') + + // detail page + if (filter) { + loadResponseStreamFromFile(path.join('snippet-service', 'configurations', 'detail', `${filter}.json`), res) + } else { + // list page + loadResponseStreamFromFile(path.join('snippet-service', 'configurations', 'list', `list.json`), res) + } + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +router.get('/api/v1/conditions', (req, res) => { + try { + checkError(req, res) + const filter = get(req.query, 'httpIdFilter', null)?.replace('/all', '') + + // detail page + if (filter) { + loadResponseStreamFromFile(path.join('snippet-service', 'conditions', 'detail', `${filter}.json`), res) + } else { + // list page + loadResponseStreamFromFile(path.join('snippet-service', 'conditions', 'list', `list.json`), res) + } + } catch (e) { + res.status(500).send(escapeHtml(e.toString())) + } +}) + +module.exports = router diff --git a/http-gateway/web/packages/mock-server/src/utils.js b/http-gateway/web/packages/mock-server/src/utils.js new file mode 100644 index 000000000..e422fcab1 --- /dev/null +++ b/http-gateway/web/packages/mock-server/src/utils.js @@ -0,0 +1,35 @@ +const { validationResult } = require('express-validator') +const fs = require('fs-extra') + +const checkError = (req, res) => { + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }) + } + + console.log(`${req.method}`, req.url) +} + +const loadResponseFromFile = (file, res) => { + const targetDirectory = `${__dirname}/data` + + res.sendFile(file, { root: targetDirectory }) +} + +const loadResponseStreamFromFile = (file, res) => { + const targetDirectory = `${__dirname}/data` + + const dataArray = fs.readJsonSync(`${targetDirectory}/${file}`) + + dataArray.forEach((data, key) => { + res.write(JSON.stringify(data) + `${key === dataArray.length - 1 ? '' : '\n\n'}`) + }) + + res.send() +} + +module.exports = { + checkError, + loadResponseFromFile, + loadResponseStreamFromFile, +} diff --git a/http-gateway/web/packages/shared-ui b/http-gateway/web/packages/shared-ui index 05fb14ee6..efddbe595 160000 --- a/http-gateway/web/packages/shared-ui +++ b/http-gateway/web/packages/shared-ui @@ -1 +1 @@ -Subproject commit 05fb14ee633229500abb840737b581d9b6c6f3f7 +Subproject commit efddbe595bb44a53b5b10eb9e406c74eec6bf141 diff --git a/http-gateway/web/playwright/package-lock.json b/http-gateway/web/playwright/package-lock.json index 99e974b6b..759d51848 100644 --- a/http-gateway/web/playwright/package-lock.json +++ b/http-gateway/web/playwright/package-lock.json @@ -8,9 +8,11 @@ "name": "hub-playwright-test", "version": "1.0.0", "license": "ISC", + "dependencies": { + "axios": "^1.7.2" + }, "devDependencies": { - "@playwright/test": "^1.44.0", - "axios": "^0.24.0" + "@playwright/test": "^1.44.0" } }, "node_modules/@playwright/test": { @@ -28,20 +30,44 @@ "node": ">=16" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/axios": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", - "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", - "dev": true, + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": { - "follow-redirects": "^1.14.4" + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" } }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, "funding": [ { "type": "individual", @@ -57,6 +83,19 @@ } } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -71,6 +110,25 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/playwright": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz", @@ -100,6 +158,11 @@ "engines": { "node": ">=16" } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" } }, "dependencies": { @@ -112,20 +175,48 @@ "playwright": "1.44.0" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "axios": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", - "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", - "dev": true, + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "requires": { - "follow-redirects": "^1.14.4" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } }, "fsevents": { "version": "2.3.2", @@ -134,6 +225,19 @@ "dev": true, "optional": true }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "playwright": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz", @@ -149,6 +253,11 @@ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz", "integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==", "dev": true + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" } } } diff --git a/http-gateway/web/playwright/package.json b/http-gateway/web/playwright/package.json index 95b91f1f9..dc563308e 100644 --- a/http-gateway/web/playwright/package.json +++ b/http-gateway/web/playwright/package.json @@ -8,13 +8,15 @@ "test": "npx playwright test", "test:ui": "npx playwright test --ui", "test:debug": "npx playwright test --debug", - "test:r": "npx playwright test devices/detail.spec.ts --debug --update-snapshots" + "test:r": "npx playwright test snippet-service/list.spec.ts --debug --update-snapshots" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { - "axios": "^0.24.0", "@playwright/test": "^1.44.0" + }, + "dependencies": { + "axios": "^1.7.2" } } diff --git a/http-gateway/web/playwright/tests/snippet-service/configuratiions/deail.spec.ts b/http-gateway/web/playwright/tests/snippet-service/configuratiions/deail.spec.ts new file mode 100644 index 000000000..a9960dacc --- /dev/null +++ b/http-gateway/web/playwright/tests/snippet-service/configuratiions/deail.spec.ts @@ -0,0 +1,17 @@ +import { test, expect, Page } from '@playwright/test' +import testId from '../../../../src/testId' + +const urlBase = 'http://localhost:3000' + +const openConfigurationItem = async (page: Page) => { + await page.goto(urlBase) + await page.getByTestId(testId.menu.snippetService.link).click() + await page.getByTestId(testId.menu.snippetService.configurations).click() + await page.getByTestId(`${testId.snippetService.configurations.list.table}-row-0`).click() +} + +test('snippet-service-configurations-detail-version', async ({ page }) => { + await openConfigurationItem(page) + + await expect(page.getByTestId(`${testId.snippetService.configurations.detail.versionSelector}`)).toBeVisible() +}) diff --git a/http-gateway/web/playwright/tests/snippet-service/configuratiions/list.spec.ts b/http-gateway/web/playwright/tests/snippet-service/configuratiions/list.spec.ts new file mode 100644 index 000000000..3f65b3d24 --- /dev/null +++ b/http-gateway/web/playwright/tests/snippet-service/configuratiions/list.spec.ts @@ -0,0 +1,13 @@ +import { test, expect } from '@playwright/test' +import testId from '../../../../src/testId' + +const urlBase = 'http://localhost:3000' + +test('snippet-service-configurations-list-open', async ({ page }) => { + await page.goto(urlBase) + await page.getByTestId(testId.menu.snippetService.link).click() + await page.getByTestId(testId.menu.snippetService.configurations).click() + + await expect(page).toHaveTitle(/Configuraions | plgd Dashboard/) + await expect(page).toHaveScreenshot({ fullPage: true, omitBackground: true }) +}) diff --git a/http-gateway/web/src/containers/App/App.i18n.ts b/http-gateway/web/src/containers/App/App.i18n.ts index c48b35d3d..7f8bb7f2a 100644 --- a/http-gateway/web/src/containers/App/App.i18n.ts +++ b/http-gateway/web/src/containers/App/App.i18n.ts @@ -121,4 +121,20 @@ export const messages = defineMessages({ id: 'app.menuConfiguration', defaultMessage: 'Configuration', }, + menuSnippetService: { + id: 'app.menuSnippetService', + defaultMessage: 'Snippet Service', + }, + menuConditions: { + id: 'app.menuConditions', + defaultMessage: 'Conditions', + }, + menuConfigurations: { + id: 'app.menuConfigurations', + defaultMessage: 'Configurations', + }, + menuAppliedConfigurations: { + id: 'app.menuAppliedConfigurations', + defaultMessage: 'Applied Configurations', + }, }) diff --git a/http-gateway/web/src/containers/Common/DetailHeaderLayout/DetailHeaderLayout.tsx b/http-gateway/web/src/containers/Common/DetailHeaderLayout/DetailHeaderLayout.tsx new file mode 100644 index 000000000..f5efa8004 --- /dev/null +++ b/http-gateway/web/src/containers/Common/DetailHeaderLayout/DetailHeaderLayout.tsx @@ -0,0 +1,76 @@ +import React, { FC, useCallback, useState } from 'react' +import isFunction from 'lodash/isFunction' + +import Button from '@shared-ui/components/Atomic/Button' +import { IconTrash } from '@shared-ui/components/Atomic/Icon' +import DeleteModal from '@shared-ui/components/Atomic/Modal/components/DeleteModal' + +import { Props } from './DetailHeaderLayout.types' + +const DetailHeaderLayout: FC = (props) => { + const { customButton, deleteApiMethod, deleteInformation, id, i18n, loading, onDeleteSuccess, onDeleteError, testIds } = props + + const [deleteModal, setDeleteModal] = useState(false) + const [deleting, setDeleting] = useState(false) + + const handleDelete = useCallback(async () => { + try { + setDeleting(true) + + await deleteApiMethod([id]) + + setDeleting(false) + setDeleteModal(false) + + isFunction(onDeleteSuccess) && onDeleteSuccess() + } catch (e: any) { + setDeleting(false) + setDeleteModal(false) + + isFunction(onDeleteError) && onDeleteError(e) + } + }, [deleteApiMethod, id, onDeleteError, onDeleteSuccess]) + + return ( +
+ {customButton} + + setDeleteModal(false), + variant: 'tertiary', + }, + { + dataTestId: testIds?.deleteButtonConfirm, + label: i18n.delete, + loading: deleting, + loadingText: i18n.deleting, + onClick: handleDelete, + variant: 'primary', + }, + ]} + onClose={() => setDeleteModal(false)} + show={deleteModal} + subTitle={i18n.subTitle} + title={i18n.title} + /> +
+ ) +} + +DetailHeaderLayout.displayName = 'DetailHeaderLayout' + +export default DetailHeaderLayout diff --git a/http-gateway/web/src/containers/Common/DetailHeaderLayout/DetailHeaderLayout.types.ts b/http-gateway/web/src/containers/Common/DetailHeaderLayout/DetailHeaderLayout.types.ts new file mode 100644 index 000000000..25875be52 --- /dev/null +++ b/http-gateway/web/src/containers/Common/DetailHeaderLayout/DetailHeaderLayout.types.ts @@ -0,0 +1,27 @@ +import { ReactNode } from 'react' + +import { DeleteInformationType } from '@shared-ui/components/Atomic/Modal/components/DeleteModal/DeleteModal.types' + +export type Props = { + customButton?: ReactNode + deleteApiMethod: (ids: string[]) => Promise + deleteInformation: DeleteInformationType[] + id: string + loading: boolean + i18n: { + cancel: string + delete: string + deleting: string + id: string + name: string + subTitle: string + title: string + } + onDeleteSuccess?: () => void + onDeleteError?: (error: any) => void + testIds?: { + deleteButton?: string + deleteButtonCancel?: string + deleteButtonConfirm?: string + } +} diff --git a/http-gateway/web/src/containers/Common/DetailHeaderLayout/index.ts b/http-gateway/web/src/containers/Common/DetailHeaderLayout/index.ts new file mode 100644 index 000000000..7b4b3ccaa --- /dev/null +++ b/http-gateway/web/src/containers/Common/DetailHeaderLayout/index.ts @@ -0,0 +1,2 @@ +export { default } from './DetailHeaderLayout' +export * from './DetailHeaderLayout' diff --git a/http-gateway/web/src/containers/Common/PageLayout/PageLayout.tsx b/http-gateway/web/src/containers/Common/PageLayout/PageLayout.tsx index 2865aa6a8..c19fc2ac7 100644 --- a/http-gateway/web/src/containers/Common/PageLayout/PageLayout.tsx +++ b/http-gateway/web/src/containers/Common/PageLayout/PageLayout.tsx @@ -3,6 +3,7 @@ import { useIntl } from 'react-intl' import isFunction from 'lodash/isFunction' import ReactDOM from 'react-dom' import { useRecoilState } from 'recoil' +import get from 'lodash/get' import { default as PageLayoutShared } from '@shared-ui/components/Atomic/PageLayout/PageLayout' import Footer from '@shared-ui/components/Layout/Footer' @@ -18,7 +19,7 @@ import { messages as t } from '@/containers/App/App.i18n' const PageLayout = forwardRef((props, ref) => { const { formatMessage: _ } = useIntl() - const { children, breadcrumbs, deviceId, notFound, pendingCommands, innerPortalTarget, size, ...rest } = props + const { children, breadcrumbs, deviceId, notFound, pendingCommands, innerPortalTarget, size, headlineCustomContent, ...rest } = props const { footerExpanded, setFooterExpanded, collapsed } = useContext(AppContext) const [isDomReady, setIsDomReady] = useState(false) @@ -37,7 +38,7 @@ const PageLayout = forwardRef((props, ref) => { collapsed={collapsed} footer={