Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IoT prototype #92

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions examples/use-cases/iot/cli/Dockerfile
Original file line number Diff line number Diff line change
@@ -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/[email protected]
RUN go install github.com/nats-io/nsc/[email protected]
RUN go install github.com/nats-io/nats-server/[email protected]

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"]
65 changes: 65 additions & 0 deletions examples/use-cases/iot/cli/lightbulb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
signal
} from "https://deno.land/[email protected]/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;
}
43 changes: 43 additions & 0 deletions examples/use-cases/iot/cli/main.sh
Original file line number Diff line number Diff line change
@@ -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

23 changes: 23 additions & 0 deletions examples/use-cases/iot/cli/n1.conf
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions examples/use-cases/iot/cli/n2.conf
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions examples/use-cases/iot/cli/n3.conf
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions examples/use-cases/iot/cli/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
connect,
credsAuthenticator,
StringCodec,
} from "https://deno.land/x/[email protected]/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...")
77 changes: 77 additions & 0 deletions examples/use-cases/iot/cli/setup-accounts.sh
Original file line number Diff line number Diff line change
@@ -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

18 changes: 18 additions & 0 deletions examples/use-cases/iot/cli/setup-operator.sh
Original file line number Diff line number Diff line change
@@ -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

31 changes: 31 additions & 0 deletions examples/use-cases/iot/cli/start-lightbulb.sh
Original file line number Diff line number Diff line change
@@ -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'
Loading