From 2fd97bd99efed30bfd9686646ada22d0103ab870 Mon Sep 17 00:00:00 2001 From: Byron Ruth Date: Fri, 13 Jan 2023 13:27:42 -0500 Subject: [PATCH] Add IoT prototype Signed-off-by: Byron Ruth --- examples/use-cases/iot/cli/Dockerfile | 21 +++++ examples/use-cases/iot/cli/lightbulb.ts | 65 ++++++++++++++++ examples/use-cases/iot/cli/main.sh | 43 +++++++++++ examples/use-cases/iot/cli/n1.conf | 23 ++++++ examples/use-cases/iot/cli/n2.conf | 23 ++++++ examples/use-cases/iot/cli/n3.conf | 23 ++++++ examples/use-cases/iot/cli/service.ts | 48 ++++++++++++ examples/use-cases/iot/cli/setup-accounts.sh | 77 +++++++++++++++++++ examples/use-cases/iot/cli/setup-operator.sh | 18 +++++ examples/use-cases/iot/cli/start-lightbulb.sh | 31 ++++++++ examples/use-cases/iot/cli/start-service.sh | 19 +++++ examples/use-cases/iot/docker-compose.yaml | 5 ++ 12 files changed, 396 insertions(+) create mode 100644 examples/use-cases/iot/cli/Dockerfile create mode 100644 examples/use-cases/iot/cli/lightbulb.ts create mode 100644 examples/use-cases/iot/cli/main.sh create mode 100644 examples/use-cases/iot/cli/n1.conf create mode 100644 examples/use-cases/iot/cli/n2.conf create mode 100644 examples/use-cases/iot/cli/n3.conf create mode 100644 examples/use-cases/iot/cli/service.ts create mode 100644 examples/use-cases/iot/cli/setup-accounts.sh create mode 100644 examples/use-cases/iot/cli/setup-operator.sh create mode 100644 examples/use-cases/iot/cli/start-lightbulb.sh create mode 100644 examples/use-cases/iot/cli/start-service.sh create mode 100644 examples/use-cases/iot/docker-compose.yaml diff --git a/examples/use-cases/iot/cli/Dockerfile b/examples/use-cases/iot/cli/Dockerfile new file mode 100644 index 00000000..955b363e --- /dev/null +++ b/examples/use-cases/iot/cli/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.19-alpine3.17 AS build + +RUN apk update && apk add git + +RUN go install github.com/nats-io/natscli/nats@v0.0.35 +RUN go install github.com/nats-io/nsc/v2@v2.7.6 +RUN go install github.com/nats-io/nats-server/v2@v2.9.11 + +FROM denoland/deno:alpine + +RUN apk update && apk add bash curl + +COPY --from=build /go/bin/nats-server /usr/local/bin/ +COPY --from=build /go/bin/nsc /usr/local/bin/ +COPY --from=build /go/bin/nats /usr/local/bin/ + +COPY . . + +ENTRYPOINT ["bash"] + +CMD ["main.sh"] diff --git a/examples/use-cases/iot/cli/lightbulb.ts b/examples/use-cases/iot/cli/lightbulb.ts new file mode 100644 index 00000000..1e76c735 --- /dev/null +++ b/examples/use-cases/iot/cli/lightbulb.ts @@ -0,0 +1,65 @@ +import { + signal +} from "https://deno.land/std@0.171.0/signal/mod.ts"; + +import { + connect +} from 'npm:mqtt'; + +// Get the passed NATS_URL or fallback to the default. This can be +// a comma-separated string. +const url = Deno.env.get("MQTT_URL"); +const username = Deno.env.get("IOT_USERNAME"); +const password = Deno.env.get("IOT_PASSWORD"); + +console.log(`Connecting to NATS via MQTT: ${username} -> ${url}...`) + +// Create a client connection to an available NATS server. +const mc = connect(url, { + username: username, + password: password, +}); + +// Initialize the default state to be off. +let state = 0; + +mc.on('connect', () => { + console.log('Connected to NATS via MQTT...') + // Indicate the device is connected. + mc.publish(`iot/${username}/events/connected`); + + // Subscribe to commands to change behavior. + mc.subscribe(`iot/${username}/commands/#`); + + mc.on('message', (topic: string) => { + console.log(`Received by ${username}... ${topic}`) + switch (topic) { + case `iot/${username}/commands/on`: + if (state === 0) { + state = 1 + mc.publish(`iot/${username}/events/on`) + } + break; + + case `iot/${username}/commands/off`: + if (state === 1) { + state = 0 + mc.publish(`iot/${username}/events/off`) + } + break; + + default: + console.log(`unknown commands: ${topic}`) + } + }) +}); + +// Block until an interrupt occurs. +const sig = signal("SIGINT"); +for await (const s of sig) { + console.log('Closing MQTT connection...') + // Indicate the device is disconnected. + mc.publish(`iot/${username}/events/disconnected`); + mc.end(); + break; +} diff --git a/examples/use-cases/iot/cli/main.sh b/examples/use-cases/iot/cli/main.sh new file mode 100644 index 00000000..c2fc0740 --- /dev/null +++ b/examples/use-cases/iot/cli/main.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +set -euo pipefail + +export NATS_URL="nats://localhost:4222,nats://localhost:4223,nats://localhost:4224" +export MQTT_URL="nats://localhost:1883" + +# Setup the operator and generate the resolver configuration. +sh setup-operator.sh + +# Setup the `service` and `customer-1` accounts. +sh setup-accounts.sh + +# Start the cluster and wait a few seconds to choose the leader. +nats-server -c n1.conf > /dev/null 2>&1 & +nats-server -c n2.conf > /dev/null 2>&1 & +nats-server -c n3.conf > /dev/null 2>&1 & + +sleep 3 + +# Ensure the cluster is healthy. +curl --fail --silent \ + --retry 5 \ + --retry-delay 1 \ + http://localhost:8222/healthz > /dev/null + +# Push the accounts to the cluster. +nsc push -A + +# Create a admin user for the customer so inspect traffic. +nsc add user --account customer-1 -K admin customer-1 +nats context save customer-1 --nsc nsc://example/customer-1/customer-1 + +# Setup general subscribers to observe the messages... +nats --context customer-1 sub 'iot.>' & +nats --context customer-1 sub 'mobile.>' & + +echo 'Starting the lightbulb device...' +sh start-lightbulb.sh + +echo 'Starting the service...' +sh start-service.sh + diff --git a/examples/use-cases/iot/cli/n1.conf b/examples/use-cases/iot/cli/n1.conf new file mode 100644 index 00000000..8e5470b3 --- /dev/null +++ b/examples/use-cases/iot/cli/n1.conf @@ -0,0 +1,23 @@ +port: 4222 +http_port: 8222 +server_name: n1 + +mqtt: { + port: 1883 +} + +jetstream: { + store_dir: "./n1" +} + +cluster: { + name: c1, + port: 6222, + routes: [ + "nats-route://0.0.0.0:6222", + "nats-route://0.0.0.0:6223", + "nats-route://0.0.0.0:6224", + ], +} + +include resolver.conf diff --git a/examples/use-cases/iot/cli/n2.conf b/examples/use-cases/iot/cli/n2.conf new file mode 100644 index 00000000..9ebed328 --- /dev/null +++ b/examples/use-cases/iot/cli/n2.conf @@ -0,0 +1,23 @@ +port: 4223 +http_port: 8223 +server_name: n2 + +mqtt: { + port: 1884 +} + +jetstream: { + store_dir: "./n2" +} + +cluster: { + name: c1, + port: 6223, + routes: [ + "nats-route://0.0.0.0:6222", + "nats-route://0.0.0.0:6223", + "nats-route://0.0.0.0:6224", + ], +} + +include resolver.conf diff --git a/examples/use-cases/iot/cli/n3.conf b/examples/use-cases/iot/cli/n3.conf new file mode 100644 index 00000000..b7441863 --- /dev/null +++ b/examples/use-cases/iot/cli/n3.conf @@ -0,0 +1,23 @@ +port: 4224 +http_port: 8224 +server_name: n3 + +mqtt: { + port: 1885 +} + +jetstream: { + store_dir: "./n3" +} + +cluster: { + name: c1, + port: 6224, + routes: [ + "nats-route://0.0.0.0:6222", + "nats-route://0.0.0.0:6223", + "nats-route://0.0.0.0:6224", + ], +} + +include resolver.conf diff --git a/examples/use-cases/iot/cli/service.ts b/examples/use-cases/iot/cli/service.ts new file mode 100644 index 00000000..ea21ee90 --- /dev/null +++ b/examples/use-cases/iot/cli/service.ts @@ -0,0 +1,48 @@ +import { + connect, + credsAuthenticator, + StringCodec, +} from "https://deno.land/x/nats@v1.10.2/src/mod.ts"; + +// Get the passed NATS_URL or fallback to the default. This can be +// a comma-separated string. +const servers = Deno.env.get("NATS_URL") || "nats://localhost:4222"; +const creds = Deno.env.get("NATS_CREDS"); + +const f = await Deno.open(creds, {read: true}); +const credsData = await Deno.readAll(f); +Deno.close(f.rid); + +console.log("Loaded the creds file...") + +// Create a client connection to an available NATS server. +const nc = await connect({ + servers: servers.split(","), + authenticator: credsAuthenticator(credsData), +}); + +console.log("Service connected to NATS...") + +// NATS message payloads are byte arrays, so we need to have a codec +// to serialize and deserialize payloads in order to work with them. +// Another built-in codec is JSONCodec or you can implement your own. +const sc = StringCodec(); +const empty = sc.encode(""); + +// Publish a message with an at-most-once guarantee to turn the light on. +await nc.publish("customer-1.iot.lightbulb-1.commands.on"); + +// Emulate an at-least-once guarantee to turn the light off. Subscribe +// and wait for the event. +const sub = nc.subscribe("customer-1.iot.lightbulb-1.events.off"); +await nc.publish("customer-1.iot.lightbulb-1.commands.off"); +for await (const msg of sub) { + console.log("confirmed lightbulb is off") + break; +} + +// Finally we drain the connection which waits for any pending +// messages (published or in a subscription) to be flushed. +await nc.drain(); + +console.log("Closed service connection to NATS...") diff --git a/examples/use-cases/iot/cli/setup-accounts.sh b/examples/use-cases/iot/cli/setup-accounts.sh new file mode 100644 index 00000000..303dc9e0 --- /dev/null +++ b/examples/use-cases/iot/cli/setup-accounts.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +set -euo pipefail + +# Next we create an account for the cloud service. +nsc add account service +nsc edit account service \ + --js-disk-storage -1 \ + --js-mem-storage -1 + +nsc edit account service --sk generate + +# Next we create an account for a customer which provides isolation +# for all messages traversing IoT and mobile devices. +nsc add account customer-1 +nsc edit account customer-1 \ + --js-disk-storage -1 \ + --js-mem-storage -1 + +# Export service (request-reply) from customer account to receive +# commands from the cloud service. +nsc add export --account customer-1 \ + --service \ + --subject "iot.*.commands.>" \ + --response-type "Singleton" + +# Export event stream from customer account to be received +# by the cloud service. +nsc add export --account customer-1 \ + --subject "iot.*.events.>" + +# Import a service from the customer account in order +# to invoke commands against their devices. +nsc add import --account service \ + --service \ + --src-account customer-1 \ + --remote-subject "iot.*.commands.>" \ + --local-subject "customer-1.iot.*.commands.>" + +# Import the event stream from the customer account. +nsc add import --account service \ + --src-account customer-1 \ + --remote-subject "iot.*.events.>" \ + --local-subject "customer-1.iot.*.events.>" + +# Generate the signing key for the Mobile devices for this +# customer. +SK1=$(nsc edit account customer-1 --sk generate 2>&1 | grep -o '\(A[^"]\+\)') +nsc edit signing-key \ + --account customer-1 \ + --sk $SK1 \ + --role mobile \ + --allow-pub "iot.*.commands.>" \ + --allow-sub "iot.*.events" \ + --allow-pub-response + +# Generate the signing key for the IoT devices for this +# customer. +SK2=$(nsc edit account customer-1 --sk generate 2>&1 | grep -o '\(A[^"]\+\)') +nsc edit signing-key \ + --account customer-1 \ + --sk $SK2 \ + --role iot \ + --bearer \ + --allow-pub "iot.{{name()}}.events.>" \ + --allow-sub "iot.{{name()}}.commands.>" \ + --allow-sub "iot.{{name()}}.commands" \ + --allow-pub-response + +# Generate the signing key for the IoT devices for this +# customer. +SK3=$(nsc edit account customer-1 --sk generate 2>&1 | grep -o '\(A[^"]\+\)') +nsc edit signing-key \ + --account customer-1 \ + --sk $SK3 \ + --role admin + diff --git a/examples/use-cases/iot/cli/setup-operator.sh b/examples/use-cases/iot/cli/setup-operator.sh new file mode 100644 index 00000000..ec20eb65 --- /dev/null +++ b/examples/use-cases/iot/cli/setup-operator.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +set -euo pipefail + +# Create the operator, generate a signing key (which is a best practice), +# and initialize the default SYS account and sys user. +nsc add operator --generate-signing-key --sys --name example + +# A follow-up edit of the operator enforces signing keys are used for +# accounts as well. Setting the server URL is a convenience so that +# it does not need to be specified with call `nsc push`. +nsc edit operator --require-signing-keys \ + --account-jwt-server-url "$NATS_URL" + +# This command generates the bit of configuration to be used by the server +# to setup the embedded JWT resolver. +nsc generate config --nats-resolver --sys-account SYS > resolver.conf + diff --git a/examples/use-cases/iot/cli/start-lightbulb.sh b/examples/use-cases/iot/cli/start-lightbulb.sh new file mode 100644 index 00000000..d1c74e6e --- /dev/null +++ b/examples/use-cases/iot/cli/start-lightbulb.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +set -euo pipefail + +# Create a user for the customer's smart device. Note the use +# of -K to indicate the role and marking it as a bearer token +# which is required for MQTT. +nsc add user --account customer-1 -K iot lightbulb-1 + +# Output the bearer JWT. +nsc describe user \ + --account customer-1 \ + --name lightbulb-1 \ + --raw > lightbulb-1.jwt + +deno cache -q lightbulb.ts + +echo 'Running lightbulb...' + +# Run the lightbulb using Deno, passing the parameters +# as env variables. +MQTT_URL=nats://localhost:1883 \ +IOT_USERNAME=lightbulb-1 \ +IOT_PASSWORD=$(cat lightbulb-1.jwt) \ + deno run --allow-net --allow-env --allow-read lightbulb.ts & + +sleep 5 + +# Simulate a couple commands. +nats --context customer-1 pub 'iot.lightbulb-1.commands.on' +nats --context customer-1 pub 'iot.lightbulb-1.commands.off' diff --git a/examples/use-cases/iot/cli/start-service.sh b/examples/use-cases/iot/cli/start-service.sh new file mode 100644 index 00000000..7dc070eb --- /dev/null +++ b/examples/use-cases/iot/cli/start-service.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -euo pipefail + +# Create the user for the service. +nsc add user --account service service \ + --allow-pub "*.iot.*.commands.>" \ + --allow-sub "*.iot.*.events.>" + +# Generate the creds for the service. +nsc generate creds \ + --account service \ + --name service > service.creds + +deno cache -q service.ts + +# Run the service using Deno, passing the service creds. +NATS_CREDS=service.creds \ + deno run --allow-net --allow-env --allow-read service.ts diff --git a/examples/use-cases/iot/docker-compose.yaml b/examples/use-cases/iot/docker-compose.yaml new file mode 100644 index 00000000..36aed1b1 --- /dev/null +++ b/examples/use-cases/iot/docker-compose.yaml @@ -0,0 +1,5 @@ +version: '3.9' +services: + app: + image: ${IMAGE_TAG} +