diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..e1bd2e1 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,37 @@ +run: + timeout: 10m + +issues: + max-issues-per-linter: 100 + max-same-issues: 100 + +linters: + enable-all: true + fast: false + disable: + - lll + - wsl + - depguard + - tagliatelle + - gomnd # This linter is deprecated + - execinquery # This linter is deprecated + - exportloopref # This linter is deprecated + - gochecknoglobals + - ireturn + - exhaustruct + - wrapcheck + - musttag + - revive + - varnamelen + - nonamedreturns + - gosec + - funlen + - interfacebloat + - dupl + - err113 + +linters-settings: + gocritic: + enable-all: true + disabled-checks: + - hugeParam diff --git a/cmd/main.go b/cmd/main.go index e3a7405..1e379d6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,10 +3,10 @@ package main import ( "context" _ "embed" - "fmt" + "log" + "github.com/absmach/propeller/proplet" "github.com/absmach/propeller/task" - "github.com/absmach/propeller/worker" "github.com/google/uuid" ) @@ -28,13 +28,15 @@ func main() { }, } - fmt.Printf("task: %s\n", t.Name) + log.Printf("task: %s\n", t.Name) - w := worker.NewWasmWorker("Wasm-Worker-1") - w.StartTask(ctx, t) + w := proplet.NewWasmProplet("Wasm-Proplet-1") + if err := w.StartTask(ctx, t); err != nil { + log.Println(err) + } results, err := w.RunTask(ctx, t.ID) if err != nil { - fmt.Println(err) + log.Println(err) } - fmt.Printf("results: %v\n", results) + log.Printf("results: %v\n", results) } diff --git a/cmd/manager/start.go b/cmd/manager/start.go new file mode 100644 index 0000000..fe31451 --- /dev/null +++ b/cmd/manager/start.go @@ -0,0 +1,111 @@ +package manager + +import ( + "context" + "fmt" + "log/slog" + "net/url" + "os" + "time" + + "github.com/absmach/magistrala/pkg/jaeger" + "github.com/absmach/magistrala/pkg/prometheus" + "github.com/absmach/magistrala/pkg/server" + httpserver "github.com/absmach/magistrala/pkg/server/http" + "github.com/absmach/propeller/manager" + "github.com/absmach/propeller/manager/api" + "github.com/absmach/propeller/manager/middleware" + "github.com/absmach/propeller/pkg/mqtt" + "github.com/absmach/propeller/pkg/scheduler" + "github.com/absmach/propeller/pkg/storage" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" + "golang.org/x/sync/errgroup" +) + +const svcName = "manager" + +type Config struct { + LogLevel string + OTELURL url.URL + TraceRatio float64 + Server server.Config + InstanceID string + ChannelID string + ThingID string + ThingKey string + MQTTAddress string + MQTTQOS uint8 + MQTTTimeout time.Duration +} + +func StartManager(ctx context.Context, cancel context.CancelFunc, cfg Config) error { + g, ctx := errgroup.WithContext(ctx) + + var level slog.Level + if err := level.UnmarshalText([]byte(cfg.LogLevel)); err != nil { + return fmt.Errorf("failed to parse log level: %s", err.Error()) + } + logHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: level, + }) + logger := slog.New(logHandler) + slog.SetDefault(logger) + + var tp trace.TracerProvider + switch { + case cfg.OTELURL == (url.URL{}): + tp = noop.NewTracerProvider() + default: + sdktp, err := jaeger.NewProvider(ctx, svcName, cfg.OTELURL, "", cfg.TraceRatio) + if err != nil { + return fmt.Errorf("failed to initialize opentelemetry: %s", err.Error()) + } + defer func() { + if err := sdktp.Shutdown(ctx); err != nil { + slog.Error("error shutting down tracer provider", slog.Any("error", err)) + } + }() + tp = sdktp + } + tracer := tp.Tracer(svcName) + + mqttPubSub, err := mqtt.NewPubSub(cfg.MQTTAddress, cfg.MQTTQOS, svcName, cfg.ThingID, cfg.ThingKey, cfg.MQTTTimeout, logger) + if err != nil { + return fmt.Errorf("failed to initialize mqtt pubsub: %s", err.Error()) + } + + svc := manager.NewService( + storage.NewInMemoryStorage(), + storage.NewInMemoryStorage(), + storage.NewInMemoryStorage(), + scheduler.NewRoundRobin(), + mqttPubSub, + cfg.ChannelID, + logger, + ) + svc = middleware.Logging(logger, svc) + svc = middleware.Tracing(tracer, svc) + counter, latency := prometheus.MakeMetrics(svcName, "api") + svc = middleware.Metrics(counter, latency, svc) + + if err := svc.Subscribe(ctx); err != nil { + return fmt.Errorf("failed to subscribe to manager channel: %s", err.Error()) + } + + hs := httpserver.NewServer(ctx, cancel, svcName, cfg.Server, api.MakeHandler(svc, logger, cfg.InstanceID), logger) + + g.Go(func() error { + return hs.Start() + }) + + g.Go(func() error { + return server.StopSignalHandler(ctx, cancel, logger, svcName, hs) + }) + + if err := g.Wait(); err != nil { + logger.Error(fmt.Sprintf("%s service exited with error: %s", svcName, err)) + } + + return nil +} diff --git a/cmd/propellerd/main.go b/cmd/propellerd/main.go new file mode 100644 index 0000000..790a337 --- /dev/null +++ b/cmd/propellerd/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "log" + + "github.com/absmach/propeller/propellerd" + "github.com/spf13/cobra" +) + +func main() { + rootCmd := &cobra.Command{ + Use: "propellerd", + Short: "Propeller Daemon", + Long: `Propeller Daemon is a daemon that manages the lifecycle of Propeller components.`, + } + + managerCmd := propellerd.NewManagerCmd() + + rootCmd.AddCommand(managerCmd) + + if err := rootCmd.Execute(); err != nil { + log.Fatal(err) + } +} diff --git a/docker/.env b/docker/.env new file mode 100644 index 0000000..9c60785 --- /dev/null +++ b/docker/.env @@ -0,0 +1,200 @@ +## NginX +MG_NGINX_HTTP_PORT=80 +MG_NGINX_SSL_PORT=443 +MG_NGINX_MQTT_PORT=1883 +MG_NGINX_MQTTS_PORT=8883 + +## Nats +MG_NATS_PORT=4222 +MG_NATS_HTTP_PORT=8222 +MG_NATS_JETSTREAM_KEY=u7wFoAPgXpDueXOFldBnXDh4xjnSOyEJ2Cb8Z5SZvGLzIZ3U4exWhhoIBZHzuNvh +MG_NATS_URL=nats://nats:${MG_NATS_PORT} +# Configs for nats as MQTT broker +MG_NATS_HEALTH_CHECK=http://nats:${MG_NATS_HTTP_PORT}/healthz +MG_NATS_WS_TARGET_PATH= +MG_NATS_MQTT_QOS=1 + +## Message Broker +MG_MESSAGE_BROKER_TYPE=nats +MG_MESSAGE_BROKER_URL=${MG_NATS_URL} + +## VERNEMQ +MG_DOCKER_VERNEMQ_ALLOW_ANONYMOUS=on +MG_DOCKER_VERNEMQ_LOG__CONSOLE__LEVEL=error +MG_VERNEMQ_HEALTH_CHECK=http://vernemq:8888/health +MG_VERNEMQ_WS_TARGET_PATH=/mqtt +MG_VERNEMQ_MQTT_QOS=2 + +## MQTT Broker +MG_MQTT_BROKER_TYPE=vernemq +MG_MQTT_BROKER_HEALTH_CHECK=${MG_VERNEMQ_HEALTH_CHECK} +MG_MQTT_ADAPTER_MQTT_QOS=${MG_VERNEMQ_MQTT_QOS} +MG_MQTT_ADAPTER_MQTT_TARGET_HOST=${MG_MQTT_BROKER_TYPE} +MG_MQTT_ADAPTER_MQTT_TARGET_PORT=1883 +MG_MQTT_ADAPTER_MQTT_TARGET_HEALTH_CHECK=${MG_MQTT_BROKER_HEALTH_CHECK} +MG_MQTT_ADAPTER_WS_TARGET_HOST=${MG_MQTT_BROKER_TYPE} +MG_MQTT_ADAPTER_WS_TARGET_PORT=8080 +MG_MQTT_ADAPTER_WS_TARGET_PATH=${MG_VERNEMQ_WS_TARGET_PATH} + +## Redis +MG_REDIS_TCP_PORT=6379 +MG_REDIS_URL=redis://es-redis:${MG_REDIS_TCP_PORT}/0 + +## Event Store +MG_ES_TYPE=${MG_MESSAGE_BROKER_TYPE} +MG_ES_URL=${MG_MESSAGE_BROKER_URL} + +## Jaeger +MG_JAEGER_COLLECTOR_OTLP_ENABLED=true +MG_JAEGER_FRONTEND=16686 +MG_JAEGER_OLTP_HTTP=4318 +MG_JAEGER_URL=http://jaeger:4318/v1/traces +MG_JAEGER_TRACE_RATIO=1.0 +MG_JAEGER_MEMORY_MAX_TRACES=5000 + +## Call home +MG_SEND_TELEMETRY=true + +## Postgres +MG_POSTGRES_MAX_CONNECTIONS=100 + +## Core Services + +### Auth +MG_AUTH_LOG_LEVEL=debug +MG_AUTH_HTTP_HOST=auth +MG_AUTH_HTTP_PORT=8189 +MG_AUTH_HTTP_SERVER_CERT= +MG_AUTH_HTTP_SERVER_KEY= +MG_AUTH_GRPC_HOST=auth +MG_AUTH_GRPC_PORT=8181 +MG_AUTH_GRPC_SERVER_CERT=${GRPC_MTLS:+./ssl/certs/auth-grpc-server.crt}${GRPC_TLS:+./ssl/certs/auth-grpc-server.crt} +MG_AUTH_GRPC_SERVER_KEY=${GRPC_MTLS:+./ssl/certs/auth-grpc-server.key}${GRPC_TLS:+./ssl/certs/auth-grpc-server.key} +MG_AUTH_GRPC_SERVER_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}${GRPC_TLS:+./ssl/certs/ca.crt} +MG_AUTH_DB_HOST=auth-db +MG_AUTH_DB_PORT=5432 +MG_AUTH_DB_USER=magistrala +MG_AUTH_DB_PASS=magistrala +MG_AUTH_DB_NAME=auth +MG_AUTH_DB_SSL_MODE=disable +MG_AUTH_DB_SSL_CERT= +MG_AUTH_DB_SSL_KEY= +MG_AUTH_DB_SSL_ROOT_CERT= +MG_AUTH_SECRET_KEY=HyE2D4RUt9nnKG6v8zKEqAp6g6ka8hhZsqUpzgKvnwpXrNVQSH +MG_AUTH_ACCESS_TOKEN_DURATION="1h" +MG_AUTH_REFRESH_TOKEN_DURATION="24h" +MG_AUTH_INVITATION_DURATION="168h" +MG_AUTH_ADAPTER_INSTANCE_ID= + +#### Auth GRPC Client Config +MG_AUTH_GRPC_URL=auth:8181 +MG_AUTH_GRPC_TIMEOUT=300s +MG_AUTH_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt} +MG_AUTH_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.key} +MG_AUTH_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt} + +#### Domains Client Config +MG_DOMAINS_URL=http://auth:8189 + +### SpiceDB Datastore config +MG_SPICEDB_DB_USER=magistrala +MG_SPICEDB_DB_PASS=magistrala +MG_SPICEDB_DB_NAME=spicedb +MG_SPICEDB_DB_PORT=5432 + +### SpiceDB config +MG_SPICEDB_PRE_SHARED_KEY="12345678" +MG_SPICEDB_SCHEMA_FILE="/schema.zed" +MG_SPICEDB_HOST=magistrala-spicedb +MG_SPICEDB_PORT=50051 +MG_SPICEDB_DATASTORE_ENGINE=postgres + +### Users +MG_USERS_LOG_LEVEL=debug +MG_USERS_SECRET_KEY=HyE2D4RUt9nnKG6v8zKEqAp6g6ka8hhZsqUpzgKvnwpXrNVQSH +MG_USERS_ADMIN_EMAIL=admin@example.com +MG_USERS_ADMIN_PASSWORD=12345678 +MG_USERS_ADMIN_USERNAME=admin +MG_USERS_ADMIN_FIRST_NAME=super +MG_USERS_ADMIN_LAST_NAME=admin +MG_USERS_PASS_REGEX=^.{8,}$ +MG_USERS_ACCESS_TOKEN_DURATION=15m +MG_USERS_REFRESH_TOKEN_DURATION=24h +MG_TOKEN_RESET_ENDPOINT=/reset-request +MG_USERS_HTTP_HOST=users +MG_USERS_HTTP_PORT=9002 +MG_USERS_HTTP_SERVER_CERT= +MG_USERS_HTTP_SERVER_KEY= +MG_USERS_DB_HOST=users-db +MG_USERS_DB_PORT=5432 +MG_USERS_DB_USER=magistrala +MG_USERS_DB_PASS=magistrala +MG_USERS_DB_NAME=users +MG_USERS_DB_SSL_MODE=disable +MG_USERS_DB_SSL_CERT= +MG_USERS_DB_SSL_KEY= +MG_USERS_DB_SSL_ROOT_CERT= +MG_USERS_RESET_PWD_TEMPLATE=users.tmpl +MG_USERS_INSTANCE_ID= +MG_USERS_ALLOW_SELF_REGISTER=true +MG_USERS_DELETE_INTERVAL=24h +MG_USERS_DELETE_AFTER=720h + +### Email utility +MG_EMAIL_HOST=smtp.mailtrap.io +MG_EMAIL_PORT=2525 +MG_EMAIL_USERNAME=18bf7f70705139 +MG_EMAIL_PASSWORD=2b0d302e775b1e +MG_EMAIL_FROM_ADDRESS=from@example.com +MG_EMAIL_FROM_NAME=Example +MG_EMAIL_TEMPLATE=email.tmpl + +### Google OAuth2 +MG_GOOGLE_CLIENT_ID= +MG_GOOGLE_CLIENT_SECRET= +MG_GOOGLE_REDIRECT_URL= +MG_GOOGLE_STATE= + +### Things +MG_THINGS_LOG_LEVEL=debug +MG_THINGS_STANDALONE_ID= +MG_THINGS_STANDALONE_TOKEN= +MG_THINGS_CACHE_KEY_DURATION=10m +MG_THINGS_HTTP_HOST=things +MG_THINGS_HTTP_PORT=9000 +MG_THINGS_AUTH_GRPC_HOST=things +MG_THINGS_AUTH_GRPC_PORT=7000 +MG_THINGS_AUTH_GRPC_SERVER_CERT=${GRPC_MTLS:+./ssl/certs/things-grpc-server.crt}${GRPC_TLS:+./ssl/certs/things-grpc-server.crt} +MG_THINGS_AUTH_GRPC_SERVER_KEY=${GRPC_MTLS:+./ssl/certs/things-grpc-server.key}${GRPC_TLS:+./ssl/certs/things-grpc-server.key} +MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}${GRPC_TLS:+./ssl/certs/ca.crt} +MG_THINGS_CACHE_URL=redis://things-redis:${MG_REDIS_TCP_PORT}/0 +MG_THINGS_DB_HOST=things-db +MG_THINGS_DB_PORT=5432 +MG_THINGS_DB_USER=magistrala +MG_THINGS_DB_PASS=magistrala +MG_THINGS_DB_NAME=things +MG_THINGS_DB_SSL_MODE=disable +MG_THINGS_DB_SSL_CERT= +MG_THINGS_DB_SSL_KEY= +MG_THINGS_DB_SSL_ROOT_CERT= +MG_THINGS_INSTANCE_ID= + +#### Things Client Config +MG_THINGS_URL=http://things:9000 +MG_THINGS_AUTH_GRPC_URL=things:7000 +MG_THINGS_AUTH_GRPC_TIMEOUT=1s +MG_THINGS_AUTH_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/things-grpc-client.crt} +MG_THINGS_AUTH_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/things-grpc-client.key} +MG_THINGS_AUTH_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt} + +### MQTT +MG_MQTT_ADAPTER_LOG_LEVEL=debug +MG_MQTT_ADAPTER_MQTT_PORT=1883 +MG_MQTT_ADAPTER_FORWARDER_TIMEOUT=30s +MG_MQTT_ADAPTER_WS_PORT=8080 +MG_MQTT_ADAPTER_INSTANCE= +MG_MQTT_ADAPTER_INSTANCE_ID= +MG_MQTT_ADAPTER_ES_DB=0 + +# Docker image tag +MG_RELEASE_TAG=latest diff --git a/docker/compose.yaml b/docker/compose.yaml new file mode 100644 index 0000000..75298d8 --- /dev/null +++ b/docker/compose.yaml @@ -0,0 +1,496 @@ +name: "magistrala" + +networks: + magistrala-base-net: + driver: bridge + +volumes: + magistrala-users-db-volume: + magistrala-things-db-volume: + magistrala-things-redis-volume: + magistrala-broker-volume: + magistrala-mqtt-broker-volume: + magistrala-spicedb-db-volume: + magistrala-auth-db-volume: + +services: + spicedb: + image: "authzed/spicedb:v1.30.0" + container_name: magistrala-spicedb + command: "serve" + restart: "always" + networks: + - magistrala-base-net + ports: + - "8080:8080" + - "9091:9090" + - "50051:50051" + environment: + SPICEDB_GRPC_PRESHARED_KEY: ${MG_SPICEDB_PRE_SHARED_KEY} + SPICEDB_DATASTORE_ENGINE: ${MG_SPICEDB_DATASTORE_ENGINE} + SPICEDB_DATASTORE_CONN_URI: "${MG_SPICEDB_DATASTORE_ENGINE}://${MG_SPICEDB_DB_USER}:${MG_SPICEDB_DB_PASS}@spicedb-db:${MG_SPICEDB_DB_PORT}/${MG_SPICEDB_DB_NAME}?sslmode=disable" + depends_on: + - spicedb-migrate + + spicedb-migrate: + image: "authzed/spicedb:v1.30.0" + container_name: magistrala-spicedb-migrate + command: "migrate head" + restart: "on-failure" + networks: + - magistrala-base-net + environment: + SPICEDB_DATASTORE_ENGINE: ${MG_SPICEDB_DATASTORE_ENGINE} + SPICEDB_DATASTORE_CONN_URI: "${MG_SPICEDB_DATASTORE_ENGINE}://${MG_SPICEDB_DB_USER}:${MG_SPICEDB_DB_PASS}@spicedb-db:${MG_SPICEDB_DB_PORT}/${MG_SPICEDB_DB_NAME}?sslmode=disable" + depends_on: + - spicedb-db + + spicedb-db: + image: "postgres:16.2-alpine" + container_name: magistrala-spicedb-db + networks: + - magistrala-base-net + ports: + - "6010:5432" + environment: + POSTGRES_USER: ${MG_SPICEDB_DB_USER} + POSTGRES_PASSWORD: ${MG_SPICEDB_DB_PASS} + POSTGRES_DB: ${MG_SPICEDB_DB_NAME} + volumes: + - magistrala-spicedb-db-volume:/var/lib/postgresql/data + + auth-db: + image: postgres:16.2-alpine + container_name: magistrala-auth-db + restart: on-failure + ports: + - 6004:5432 + environment: + POSTGRES_USER: ${MG_AUTH_DB_USER} + POSTGRES_PASSWORD: ${MG_AUTH_DB_PASS} + POSTGRES_DB: ${MG_AUTH_DB_NAME} + networks: + - magistrala-base-net + volumes: + - magistrala-auth-db-volume:/var/lib/postgresql/data + + auth: + image: ghcr.io/absmach/magistrala/auth:${MG_RELEASE_TAG} + container_name: magistrala-auth + depends_on: + - auth-db + - spicedb + expose: + - ${MG_AUTH_GRPC_PORT} + restart: on-failure + environment: + MG_AUTH_LOG_LEVEL: ${MG_AUTH_LOG_LEVEL} + MG_SPICEDB_SCHEMA_FILE: ${MG_SPICEDB_SCHEMA_FILE} + MG_SPICEDB_PRE_SHARED_KEY: ${MG_SPICEDB_PRE_SHARED_KEY} + MG_SPICEDB_HOST: ${MG_SPICEDB_HOST} + MG_SPICEDB_PORT: ${MG_SPICEDB_PORT} + MG_AUTH_ACCESS_TOKEN_DURATION: ${MG_AUTH_ACCESS_TOKEN_DURATION} + MG_AUTH_REFRESH_TOKEN_DURATION: ${MG_AUTH_REFRESH_TOKEN_DURATION} + MG_AUTH_INVITATION_DURATION: ${MG_AUTH_INVITATION_DURATION} + MG_AUTH_SECRET_KEY: ${MG_AUTH_SECRET_KEY} + MG_AUTH_HTTP_HOST: ${MG_AUTH_HTTP_HOST} + MG_AUTH_HTTP_PORT: ${MG_AUTH_HTTP_PORT} + MG_AUTH_HTTP_SERVER_CERT: ${MG_AUTH_HTTP_SERVER_CERT} + MG_AUTH_HTTP_SERVER_KEY: ${MG_AUTH_HTTP_SERVER_KEY} + MG_AUTH_GRPC_HOST: ${MG_AUTH_GRPC_HOST} + MG_AUTH_GRPC_PORT: ${MG_AUTH_GRPC_PORT} + ## Compose supports parameter expansion in environment, + ## Eg: ${VAR:+replacement} or ${VAR+replacement} -> replacement if VAR is set and non-empty, otherwise empty + ## Eg :${VAR:-default} or ${VAR-default} -> value of VAR if set and non-empty, otherwise default + MG_AUTH_GRPC_SERVER_CERT: ${MG_AUTH_GRPC_SERVER_CERT:+/auth-grpc-server.crt} + MG_AUTH_GRPC_SERVER_KEY: ${MG_AUTH_GRPC_SERVER_KEY:+/auth-grpc-server.key} + MG_AUTH_GRPC_SERVER_CA_CERTS: ${MG_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt} + MG_AUTH_GRPC_CLIENT_CA_CERTS: ${MG_AUTH_GRPC_CLIENT_CA_CERTS:+/auth-grpc-client-ca.crt} + MG_AUTH_DB_HOST: ${MG_AUTH_DB_HOST} + MG_AUTH_DB_PORT: ${MG_AUTH_DB_PORT} + MG_AUTH_DB_USER: ${MG_AUTH_DB_USER} + MG_AUTH_DB_PASS: ${MG_AUTH_DB_PASS} + MG_AUTH_DB_NAME: ${MG_AUTH_DB_NAME} + MG_AUTH_DB_SSL_MODE: ${MG_AUTH_DB_SSL_MODE} + MG_AUTH_DB_SSL_CERT: ${MG_AUTH_DB_SSL_CERT} + MG_AUTH_DB_SSL_KEY: ${MG_AUTH_DB_SSL_KEY} + MG_AUTH_DB_SSL_ROOT_CERT: ${MG_AUTH_DB_SSL_ROOT_CERT} + MG_JAEGER_URL: ${MG_JAEGER_URL} + MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} + MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY} + MG_AUTH_ADAPTER_INSTANCE_ID: ${MG_AUTH_ADAPTER_INSTANCE_ID} + MG_ES_URL: ${MG_ES_URL} + ports: + - ${MG_AUTH_HTTP_PORT}:${MG_AUTH_HTTP_PORT} + - ${MG_AUTH_GRPC_PORT}:${MG_AUTH_GRPC_PORT} + networks: + - magistrala-base-net + volumes: + - ./spicedb/schema.zed:${MG_SPICEDB_SCHEMA_FILE} + # Auth gRPC mTLS server certificates + - type: bind + source: ${MG_AUTH_GRPC_SERVER_CERT:-ssl/certs/dummy/server_cert} + target: /auth-grpc-server${MG_AUTH_GRPC_SERVER_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_SERVER_KEY:-ssl/certs/dummy/server_key} + target: /auth-grpc-server${MG_AUTH_GRPC_SERVER_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca_certs} + target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_CA_CERTS:-ssl/certs/dummy/client_ca_certs} + target: /auth-grpc-client-ca${MG_AUTH_GRPC_CLIENT_CA_CERTS:+.crt} + bind: + create_host_path: true + + nginx: + image: nginx:1.25.4-alpine + container_name: magistrala-nginx + restart: on-failure + volumes: + - ./nginx/nginx-${AUTH-key}.conf:/etc/nginx/nginx.conf.template + - ./nginx/entrypoint.sh:/docker-entrypoint.d/entrypoint.sh + - ./nginx/snippets:/etc/nginx/snippets + - ./ssl/authorization.js:/etc/nginx/authorization.js + - type: bind + source: ${MG_NGINX_SERVER_CERT:-./ssl/certs/magistrala-server.crt} + target: /etc/ssl/certs/magistrala-server.crt + - type: bind + source: ${MG_NGINX_SERVER_KEY:-./ssl/certs/magistrala-server.key} + target: /etc/ssl/private/magistrala-server.key + - type: bind + source: ${MG_NGINX_SERVER_CLIENT_CA:-./ssl/certs/ca.crt} + target: /etc/ssl/certs/ca.crt + - type: bind + source: ${MG_NGINX_SERVER_DHPARAM:-./ssl/dhparam.pem} + target: /etc/ssl/certs/dhparam.pem + ports: + - ${MG_NGINX_HTTP_PORT}:${MG_NGINX_HTTP_PORT} + - ${MG_NGINX_SSL_PORT}:${MG_NGINX_SSL_PORT} + - ${MG_NGINX_MQTT_PORT}:${MG_NGINX_MQTT_PORT} + - ${MG_NGINX_MQTTS_PORT}:${MG_NGINX_MQTTS_PORT} + networks: + - magistrala-base-net + env_file: + - .env + depends_on: + - auth + - things + - users + - mqtt-adapter + + things-db: + image: postgres:16.2-alpine + container_name: magistrala-things-db + restart: on-failure + command: postgres -c "max_connections=${MG_POSTGRES_MAX_CONNECTIONS}" + environment: + POSTGRES_USER: ${MG_THINGS_DB_USER} + POSTGRES_PASSWORD: ${MG_THINGS_DB_PASS} + POSTGRES_DB: ${MG_THINGS_DB_NAME} + MG_POSTGRES_MAX_CONNECTIONS: ${MG_POSTGRES_MAX_CONNECTIONS} + networks: + - magistrala-base-net + ports: + - 6006:5432 + volumes: + - magistrala-things-db-volume:/var/lib/postgresql/data + + things-redis: + image: redis:7.2.4-alpine + container_name: magistrala-things-redis + restart: on-failure + networks: + - magistrala-base-net + volumes: + - magistrala-things-redis-volume:/data + + things: + image: ghcr.io/absmach/magistrala/things:${MG_RELEASE_TAG} + container_name: magistrala-things + depends_on: + - things-db + - users + - auth + - nats + restart: on-failure + environment: + MG_THINGS_LOG_LEVEL: ${MG_THINGS_LOG_LEVEL} + MG_THINGS_STANDALONE_ID: ${MG_THINGS_STANDALONE_ID} + MG_THINGS_STANDALONE_TOKEN: ${MG_THINGS_STANDALONE_TOKEN} + MG_THINGS_CACHE_KEY_DURATION: ${MG_THINGS_CACHE_KEY_DURATION} + MG_THINGS_HTTP_HOST: ${MG_THINGS_HTTP_HOST} + MG_THINGS_HTTP_PORT: ${MG_THINGS_HTTP_PORT} + MG_THINGS_AUTH_GRPC_HOST: ${MG_THINGS_AUTH_GRPC_HOST} + MG_THINGS_AUTH_GRPC_PORT: ${MG_THINGS_AUTH_GRPC_PORT} + ## Compose supports parameter expansion in environment, + ## Eg: ${VAR:+replacement} or ${VAR+replacement} -> replacement if VAR is set and non-empty, otherwise empty + ## Eg :${VAR:-default} or ${VAR-default} -> value of VAR if set and non-empty, otherwise default + MG_THINGS_AUTH_GRPC_SERVER_CERT: ${MG_THINGS_AUTH_GRPC_SERVER_CERT:+/things-grpc-server.crt} + MG_THINGS_AUTH_GRPC_SERVER_KEY: ${MG_THINGS_AUTH_GRPC_SERVER_KEY:+/things-grpc-server.key} + MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS: ${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:+/things-grpc-server-ca.crt} + MG_THINGS_AUTH_GRPC_CLIENT_CA_CERTS: ${MG_THINGS_AUTH_GRPC_CLIENT_CA_CERTS:+/things-grpc-client-ca.crt} + MG_ES_URL: ${MG_ES_URL} + MG_THINGS_CACHE_URL: ${MG_THINGS_CACHE_URL} + MG_THINGS_DB_HOST: ${MG_THINGS_DB_HOST} + MG_THINGS_DB_PORT: ${MG_THINGS_DB_PORT} + MG_THINGS_DB_USER: ${MG_THINGS_DB_USER} + MG_THINGS_DB_PASS: ${MG_THINGS_DB_PASS} + MG_THINGS_DB_NAME: ${MG_THINGS_DB_NAME} + MG_THINGS_DB_SSL_MODE: ${MG_THINGS_DB_SSL_MODE} + MG_THINGS_DB_SSL_CERT: ${MG_THINGS_DB_SSL_CERT} + MG_THINGS_DB_SSL_KEY: ${MG_THINGS_DB_SSL_KEY} + MG_THINGS_DB_SSL_ROOT_CERT: ${MG_THINGS_DB_SSL_ROOT_CERT} + MG_AUTH_GRPC_URL: ${MG_AUTH_GRPC_URL} + MG_AUTH_GRPC_TIMEOUT: ${MG_AUTH_GRPC_TIMEOUT} + MG_AUTH_GRPC_CLIENT_CERT: ${MG_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt} + MG_AUTH_GRPC_CLIENT_KEY: ${MG_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key} + MG_AUTH_GRPC_SERVER_CA_CERTS: ${MG_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt} + MG_JAEGER_URL: ${MG_JAEGER_URL} + MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} + MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY} + MG_SPICEDB_PRE_SHARED_KEY: ${MG_SPICEDB_PRE_SHARED_KEY} + MG_SPICEDB_HOST: ${MG_SPICEDB_HOST} + MG_SPICEDB_PORT: ${MG_SPICEDB_PORT} + ports: + - ${MG_THINGS_HTTP_PORT}:${MG_THINGS_HTTP_PORT} + - ${MG_THINGS_AUTH_GRPC_PORT}:${MG_THINGS_AUTH_GRPC_PORT} + networks: + - magistrala-base-net + volumes: + # Things gRPC server certificates + - type: bind + source: ${MG_THINGS_AUTH_GRPC_SERVER_CERT:-ssl/certs/dummy/server_cert} + target: /things-grpc-server${MG_THINGS_AUTH_GRPC_SERVER_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_THINGS_AUTH_GRPC_SERVER_KEY:-ssl/certs/dummy/server_key} + target: /things-grpc-server${MG_THINGS_AUTH_GRPC_SERVER_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca_certs} + target: /things-grpc-server-ca${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_THINGS_AUTH_GRPC_CLIENT_CA_CERTS:-ssl/certs/dummy/client_ca_certs} + target: /things-grpc-client-ca${MG_THINGS_AUTH_GRPC_CLIENT_CA_CERTS:+.crt} + bind: + create_host_path: true + # Auth gRPC client certificates + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + + users-db: + image: postgres:16.2-alpine + container_name: magistrala-users-db + restart: on-failure + command: postgres -c "max_connections=${MG_POSTGRES_MAX_CONNECTIONS}" + environment: + POSTGRES_USER: ${MG_USERS_DB_USER} + POSTGRES_PASSWORD: ${MG_USERS_DB_PASS} + POSTGRES_DB: ${MG_USERS_DB_NAME} + MG_POSTGRES_MAX_CONNECTIONS: ${MG_POSTGRES_MAX_CONNECTIONS} + ports: + - 6000:5432 + networks: + - magistrala-base-net + volumes: + - magistrala-users-db-volume:/var/lib/postgresql/data + + users: + image: magistrala/users:${MG_RELEASE_TAG} + container_name: magistrala-users + depends_on: + - users-db + - auth + - nats + restart: on-failure + environment: + MG_USERS_LOG_LEVEL: ${MG_USERS_LOG_LEVEL} + MG_USERS_SECRET_KEY: ${MG_USERS_SECRET_KEY} + MG_USERS_ADMIN_EMAIL: ${MG_USERS_ADMIN_EMAIL} + MG_USERS_ADMIN_PASSWORD: ${MG_USERS_ADMIN_PASSWORD} + MG_USERS_ADMIN_USERNAME: ${MG_USERS_ADMIN_USERNAME} + MG_USERS_ADMIN_FIRST_NAME: ${MG_USERS_ADMIN_FIRST_NAME} + MG_USERS_ADMIN_LAST_NAME: ${MG_USERS_ADMIN_LAST_NAME} + MG_USERS_PASS_REGEX: ${MG_USERS_PASS_REGEX} + MG_USERS_ACCESS_TOKEN_DURATION: ${MG_USERS_ACCESS_TOKEN_DURATION} + MG_USERS_REFRESH_TOKEN_DURATION: ${MG_USERS_REFRESH_TOKEN_DURATION} + MG_TOKEN_RESET_ENDPOINT: ${MG_TOKEN_RESET_ENDPOINT} + MG_USERS_HTTP_HOST: ${MG_USERS_HTTP_HOST} + MG_USERS_HTTP_PORT: ${MG_USERS_HTTP_PORT} + MG_USERS_HTTP_SERVER_CERT: ${MG_USERS_HTTP_SERVER_CERT} + MG_USERS_HTTP_SERVER_KEY: ${MG_USERS_HTTP_SERVER_KEY} + MG_USERS_DB_HOST: ${MG_USERS_DB_HOST} + MG_USERS_DB_PORT: ${MG_USERS_DB_PORT} + MG_USERS_DB_USER: ${MG_USERS_DB_USER} + MG_USERS_DB_PASS: ${MG_USERS_DB_PASS} + MG_USERS_DB_NAME: ${MG_USERS_DB_NAME} + MG_USERS_DB_SSL_MODE: ${MG_USERS_DB_SSL_MODE} + MG_USERS_DB_SSL_CERT: ${MG_USERS_DB_SSL_CERT} + MG_USERS_DB_SSL_KEY: ${MG_USERS_DB_SSL_KEY} + MG_USERS_DB_SSL_ROOT_CERT: ${MG_USERS_DB_SSL_ROOT_CERT} + MG_USERS_ALLOW_SELF_REGISTER: ${MG_USERS_ALLOW_SELF_REGISTER} + MG_EMAIL_HOST: ${MG_EMAIL_HOST} + MG_EMAIL_PORT: ${MG_EMAIL_PORT} + MG_EMAIL_USERNAME: ${MG_EMAIL_USERNAME} + MG_EMAIL_PASSWORD: ${MG_EMAIL_PASSWORD} + MG_EMAIL_FROM_ADDRESS: ${MG_EMAIL_FROM_ADDRESS} + MG_EMAIL_FROM_NAME: ${MG_EMAIL_FROM_NAME} + MG_EMAIL_TEMPLATE: ${MG_EMAIL_TEMPLATE} + MG_ES_URL: ${MG_ES_URL} + MG_JAEGER_URL: ${MG_JAEGER_URL} + MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} + MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY} + MG_AUTH_GRPC_URL: ${MG_AUTH_GRPC_URL} + MG_AUTH_GRPC_TIMEOUT: ${MG_AUTH_GRPC_TIMEOUT} + MG_AUTH_GRPC_CLIENT_CERT: ${MG_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt} + MG_AUTH_GRPC_CLIENT_KEY: ${MG_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key} + MG_AUTH_GRPC_SERVER_CA_CERTS: ${MG_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt} + MG_GOOGLE_CLIENT_ID: ${MG_GOOGLE_CLIENT_ID} + MG_GOOGLE_CLIENT_SECRET: ${MG_GOOGLE_CLIENT_SECRET} + MG_GOOGLE_REDIRECT_URL: ${MG_GOOGLE_REDIRECT_URL} + MG_GOOGLE_STATE: ${MG_GOOGLE_STATE} + MG_USERS_DELETE_INTERVAL: ${MG_USERS_DELETE_INTERVAL} + MG_USERS_DELETE_AFTER: ${MG_USERS_DELETE_AFTER} + MG_SPICEDB_PRE_SHARED_KEY: ${MG_SPICEDB_PRE_SHARED_KEY} + MG_SPICEDB_HOST: ${MG_SPICEDB_HOST} + MG_SPICEDB_PORT: ${MG_SPICEDB_PORT} + ports: + - ${MG_USERS_HTTP_PORT}:${MG_USERS_HTTP_PORT} + networks: + - magistrala-base-net + volumes: + - ./templates/${MG_USERS_RESET_PWD_TEMPLATE}:/email.tmpl + # Auth gRPC client certificates + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + + jaeger: + image: jaegertracing/all-in-one:1.60 + container_name: magistrala-jaeger + environment: + COLLECTOR_OTLP_ENABLED: ${MG_JAEGER_COLLECTOR_OTLP_ENABLED} + command: --memory.max-traces ${MG_JAEGER_MEMORY_MAX_TRACES} + ports: + - ${MG_JAEGER_FRONTEND}:${MG_JAEGER_FRONTEND} + - ${MG_JAEGER_OLTP_HTTP}:${MG_JAEGER_OLTP_HTTP} + networks: + - magistrala-base-net + + mqtt-adapter: + image: ghcr.io/absmach/magistrala/mqtt:${MG_RELEASE_TAG} + container_name: magistrala-mqtt + depends_on: + - things + - vernemq + - nats + restart: on-failure + environment: + MG_MQTT_ADAPTER_LOG_LEVEL: ${MG_MQTT_ADAPTER_LOG_LEVEL} + MG_MQTT_ADAPTER_MQTT_PORT: ${MG_MQTT_ADAPTER_MQTT_PORT} + MG_MQTT_ADAPTER_MQTT_TARGET_HOST: ${MG_MQTT_ADAPTER_MQTT_TARGET_HOST} + MG_MQTT_ADAPTER_MQTT_TARGET_PORT: ${MG_MQTT_ADAPTER_MQTT_TARGET_PORT} + MG_MQTT_ADAPTER_FORWARDER_TIMEOUT: ${MG_MQTT_ADAPTER_FORWARDER_TIMEOUT} + MG_MQTT_ADAPTER_MQTT_TARGET_HEALTH_CHECK: ${MG_MQTT_ADAPTER_MQTT_TARGET_HEALTH_CHECK} + MG_MQTT_ADAPTER_MQTT_QOS: ${MG_MQTT_ADAPTER_MQTT_QOS} + MG_MQTT_ADAPTER_WS_PORT: ${MG_MQTT_ADAPTER_WS_PORT} + MG_MQTT_ADAPTER_INSTANCE_ID: ${MG_MQTT_ADAPTER_INSTANCE_ID} + MG_MQTT_ADAPTER_WS_TARGET_HOST: ${MG_MQTT_ADAPTER_WS_TARGET_HOST} + MG_MQTT_ADAPTER_WS_TARGET_PORT: ${MG_MQTT_ADAPTER_WS_TARGET_PORT} + MG_MQTT_ADAPTER_WS_TARGET_PATH: ${MG_MQTT_ADAPTER_WS_TARGET_PATH} + MG_MQTT_ADAPTER_INSTANCE: ${MG_MQTT_ADAPTER_INSTANCE} + MG_ES_URL: ${MG_ES_URL} + MG_THINGS_AUTH_GRPC_URL: ${MG_THINGS_AUTH_GRPC_URL} + MG_THINGS_AUTH_GRPC_TIMEOUT: ${MG_THINGS_AUTH_GRPC_TIMEOUT} + MG_THINGS_AUTH_GRPC_CLIENT_CERT: ${MG_THINGS_AUTH_GRPC_CLIENT_CERT:+/things-grpc-client.crt} + MG_THINGS_AUTH_GRPC_CLIENT_KEY: ${MG_THINGS_AUTH_GRPC_CLIENT_KEY:+/things-grpc-client.key} + MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS: ${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:+/things-grpc-server-ca.crt} + MG_JAEGER_URL: ${MG_JAEGER_URL} + MG_MESSAGE_BROKER_URL: ${MG_MESSAGE_BROKER_URL} + MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO} + MG_SEND_TELEMETRY: ${MG_SEND_TELEMETRY} + networks: + - magistrala-base-net + volumes: + # Things gRPC mTLS client certificates + - type: bind + source: ${MG_THINGS_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert} + target: /things-grpc-client${MG_THINGS_AUTH_GRPC_CLIENT_CERT:+.crt} + bind: + create_host_path: true + - type: bind + source: ${MG_THINGS_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key} + target: /things-grpc-client${MG_THINGS_AUTH_GRPC_CLIENT_KEY:+.key} + bind: + create_host_path: true + - type: bind + source: ${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca} + target: /things-grpc-server-ca${MG_THINGS_AUTH_GRPC_SERVER_CA_CERTS:+.crt} + bind: + create_host_path: true + + vernemq: + image: magistrala/vernemq:${MG_RELEASE_TAG} + container_name: magistrala-vernemq + restart: on-failure + environment: + DOCKER_VERNEMQ_ALLOW_ANONYMOUS: ${MG_DOCKER_VERNEMQ_ALLOW_ANONYMOUS} + DOCKER_VERNEMQ_LOG__CONSOLE__LEVEL: ${MG_DOCKER_VERNEMQ_LOG__CONSOLE__LEVEL} + networks: + - magistrala-base-net + volumes: + - magistrala-mqtt-broker-volume:/var/lib/vernemq + + nats: + image: nats:2.10.9-alpine + container_name: magistrala-nats + restart: on-failure + command: "--config=/etc/nats/nats.conf" + environment: + - MG_NATS_PORT=${MG_NATS_PORT} + - MG_NATS_HTTP_PORT=${MG_NATS_HTTP_PORT} + - MG_NATS_JETSTREAM_KEY=${MG_NATS_JETSTREAM_KEY} + ports: + - ${MG_NATS_PORT}:${MG_NATS_PORT} + - ${MG_NATS_HTTP_PORT}:${MG_NATS_HTTP_PORT} + volumes: + - magistrala-broker-volume:/data + - ./nats:/etc/nats + networks: + - magistrala-base-net diff --git a/docker/nats/nats.conf b/docker/nats/nats.conf new file mode 100644 index 0000000..10e590c --- /dev/null +++ b/docker/nats/nats.conf @@ -0,0 +1,24 @@ +server_name: "nats_internal_broker" +max_payload: 1MB +max_connections: 1M +port: $MG_NATS_PORT +http_port: $MG_NATS_HTTP_PORT +trace: true + +jetstream { + store_dir: "/data" + cipher: "aes" + key: $MG_NATS_JETSTREAM_KEY + max_mem: 1G +} + +mqtt { + port: 1883 + max_ack_pending: 1 +} + +websocket { + port: 8080 + + no_tls: true +} diff --git a/docker/nginx/.gitignore b/docker/nginx/.gitignore new file mode 100644 index 0000000..d17a3f6 --- /dev/null +++ b/docker/nginx/.gitignore @@ -0,0 +1,2 @@ +snippets/mqtt-upstream.conf +snippets/mqtt-ws-upstream.conf \ No newline at end of file diff --git a/docker/nginx/entrypoint.sh b/docker/nginx/entrypoint.sh new file mode 100755 index 0000000..ed647cd --- /dev/null +++ b/docker/nginx/entrypoint.sh @@ -0,0 +1,21 @@ +#!/bin/ash + +if [ -z "$MG_MQTT_CLUSTER" ] +then + envsubst '${MG_MQTT_ADAPTER_MQTT_PORT}' < /etc/nginx/snippets/mqtt-upstream-single.conf > /etc/nginx/snippets/mqtt-upstream.conf + envsubst '${MG_MQTT_ADAPTER_WS_PORT}' < /etc/nginx/snippets/mqtt-ws-upstream-single.conf > /etc/nginx/snippets/mqtt-ws-upstream.conf +else + envsubst '${MG_MQTT_ADAPTER_MQTT_PORT}' < /etc/nginx/snippets/mqtt-upstream-cluster.conf > /etc/nginx/snippets/mqtt-upstream.conf + envsubst '${MG_MQTT_ADAPTER_WS_PORT}' < /etc/nginx/snippets/mqtt-ws-upstream-cluster.conf > /etc/nginx/snippets/mqtt-ws-upstream.conf +fi + +envsubst ' + ${MG_NGINX_SERVER_NAME} + ${MG_AUTH_HTTP_PORT} + ${MG_USERS_HTTP_PORT} + ${MG_THINGS_HTTP_PORT} + ${MG_THINGS_AUTH_HTTP_PORT} + ${MG_NGINX_MQTT_PORT} + ${MG_NGINX_MQTTS_PORT}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf + +exec nginx -g "daemon off;" diff --git a/docker/nginx/nginx-key.conf b/docker/nginx/nginx-key.conf new file mode 100644 index 0000000..81cce5a --- /dev/null +++ b/docker/nginx/nginx-key.conf @@ -0,0 +1,185 @@ +# This is the default Magistrala NGINX configuration. + +user nginx; +worker_processes auto; +worker_cpu_affinity auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + # Explanation: https://serverfault.com/questions/787919/optimal-value-for-nginx-worker-connections + # We'll keep 10k connections per core (assuming one worker per core) + worker_connections 10000; +} + +http { + include snippets/http_access_log.conf; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + # Include single-node or multiple-node (cluster) upstream + include snippets/mqtt-ws-upstream.conf; + + server { + listen 80 default_server; + listen [::]:80 default_server; + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + http2 on; + + set $dynamic_server_name "$MG_NGINX_SERVER_NAME"; + + if ($dynamic_server_name = '') { + set $dynamic_server_name "localhost"; + } + + server_name $dynamic_server_name; + + include snippets/ssl.conf; + + add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header Access-Control-Allow-Origin '*'; + add_header Access-Control-Allow-Methods '*'; + add_header Access-Control-Allow-Headers '*'; + + location ~ ^/(channels)/(.+)/(things)/(.+) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + } + # Proxy pass to users & groups id to things service for listing of channels + # /users/{userID}/channels - Listing of channels belongs to userID + # /groups/{userGroupID}/channels - Listing of channels belongs to userGroupID + location ~ ^/(users|groups)/(.+)/(channels|things) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + if ($request_method = GET) { + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + break; + } + proxy_pass http://users:${MG_USERS_HTTP_PORT}; + } + + # Proxy pass to channel id to users service for listing of channels + # /channels/{channelID}/users - Listing of Users belongs to channelID + # /channels/{channelID}/groups - Listing of User Groups belongs to channelID + location ~ ^/(channels|things)/(.+)/(users|groups) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + if ($request_method = GET) { + proxy_pass http://users:${MG_USERS_HTTP_PORT}; + break; + } + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + } + + # Proxy pass to user id to auth service for listing of domains + # /users/{userID}/domains - Listing of Domains belongs to userID + location ~ ^/(users)/(.+)/(domains) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + if ($request_method = GET) { + proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; + break; + } + proxy_pass http://users:${MG_USERS_HTTP_PORT}; + } + + # Proxy pass to domain id to users service for listing of users + # /domains/{domainID}/users - Listing of Users belongs to domainID + location ~ ^/(domains)/(.+)/(users) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + if ($request_method = GET) { + proxy_pass http://users:${MG_USERS_HTTP_PORT}; + break; + } + proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; + } + + + # Proxy pass to auth service + location ~ ^/(domains) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; + } + + # Proxy pass to users service + location ~ ^/(users|groups|password|authorize|oauth/callback/[^/]+) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://users:${MG_USERS_HTTP_PORT}; + } + + location ^~ /users/policies { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://users:${MG_USERS_HTTP_PORT}/policies; + } + + # Proxy pass to things service + location ~ ^/(things|channels|connect|disconnect|identify) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + } + + location ^~ /things/policies { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://things:${MG_THINGS_HTTP_PORT}/policies; + } + + + location /health { + include snippets/proxy-headers.conf; + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + } + + location /metrics { + include snippets/proxy-headers.conf; + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + } + + # Proxy pass to magistrala-mqtt-adapter over WS + location /mqtt { + include snippets/proxy-headers.conf; + include snippets/ws-upgrade.conf; + proxy_pass http://mqtt_ws_cluster; + } + } +} + +# MQTT +stream { + include snippets/stream_access_log.conf; + + # Include single-node or multiple-node (cluster) upstream + include snippets/mqtt-upstream.conf; + + server { + listen ${MG_NGINX_MQTT_PORT}; + listen [::]:${MG_NGINX_MQTT_PORT}; + listen ${MG_NGINX_MQTTS_PORT} ssl; + listen [::]:${MG_NGINX_MQTTS_PORT} ssl; + + include snippets/ssl.conf; + + proxy_pass mqtt_cluster; + } +} + +error_log info.log info; diff --git a/docker/nginx/nginx-x509.conf b/docker/nginx/nginx-x509.conf new file mode 100644 index 0000000..10984b3 --- /dev/null +++ b/docker/nginx/nginx-x509.conf @@ -0,0 +1,202 @@ +# This is the Magistrala NGINX configuration for mututal authentication based on X.509 certifiactes. + +user nginx; +worker_processes auto; +worker_cpu_affinity auto; +pid /run/nginx.pid; +load_module /etc/nginx/modules/ngx_stream_js_module.so; +load_module /etc/nginx/modules/ngx_http_js_module.so; +include /etc/nginx/modules-enabled/*.conf; + +events { + # Explanation: https://serverfault.com/questions/787919/optimal-value-for-nginx-worker-connections + # We'll keep 10k connections per core (assuming one worker per core) + worker_connections 10000; +} + +http { + include snippets/http_access_log.conf; + + js_path "/etc/nginx/njs/"; + js_import authorization from /etc/nginx/authorization.js; + + js_set $auth_key authorization.setKey; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + # Include single-node or multiple-node (cluster) upstream + include snippets/mqtt-ws-upstream.conf; + + server { + listen 80 default_server; + listen [::]:80 default_server; + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + http2 on; + + set $dynamic_server_name "$MG_NGINX_SERVER_NAME"; + + if ($dynamic_server_name = '') { + set $dynamic_server_name "localhost"; + } + + server_name $dynamic_server_name; + + ssl_verify_client optional; + include snippets/ssl.conf; + include snippets/ssl-client.conf; + + add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header Access-Control-Allow-Origin '*'; + add_header Access-Control-Allow-Methods '*'; + add_header Access-Control-Allow-Headers '*'; + + location ~ ^/(channels)/(.+)/(things)/(.+) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + } + # Proxy pass to users & groups id to things service for listing of channels + # /users/{userID}/channels - Listing of channels belongs to userID + # /groups/{userGroupID}/channels - Listing of channels belongs to userGroupID + location ~ ^/(users|groups)/(.+)/(channels|things) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + if ($request_method = GET) { + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + break; + } + proxy_pass http://users:${MG_USERS_HTTP_PORT}; + } + + # Proxy pass to channel id to users service for listing of channels + # /channels/{channelID}/users - Listing of Users belongs to channelID + # /channels/{channelID}/groups - Listing of User Groups belongs to channelID + location ~ ^/(channels|things)/(.+)/(users|groups) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + if ($request_method = GET) { + proxy_pass http://users:${MG_USERS_HTTP_PORT}; + break; + } + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + } + + # Proxy pass to user id to auth service for listing of domains + # /users/{userID}/domains - Listing of Domains belongs to userID + location ~ ^/(users)/(.+)/(domains) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + if ($request_method = GET) { + proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; + break; + } + proxy_pass http://users:${MG_USERS_HTTP_PORT}; + } + + # Proxy pass to domain id to users service for listing of users + # /domains/{domainID}/users - Listing of Users belongs to domainID + location ~ ^/(domains)/(.+)/(users) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + if ($request_method = GET) { + proxy_pass http://users:${MG_USERS_HTTP_PORT}; + break; + } + proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; + } + + + # Proxy pass to auth service + location ~ ^/(domains) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://auth:${MG_AUTH_HTTP_PORT}; + } + + # Proxy pass to users service + location ~ ^/(users|groups|password|authorize|oauth/callback/[^/]+) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://users:${MG_USERS_HTTP_PORT}; + } + + location ^~ /users/policies { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://users:${MG_USERS_HTTP_PORT}/policies; + } + + # Proxy pass to things service + location ~ ^/(things|channels|connect|disconnect|identify) { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + } + + location ^~ /things/policies { + include snippets/proxy-headers.conf; + add_header Access-Control-Expose-Headers Location; + proxy_pass http://things:${MG_THINGS_HTTP_PORT}/policies; + } + + location /health { + include snippets/proxy-headers.conf; + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + } + + location /metrics { + include snippets/proxy-headers.conf; + proxy_pass http://things:${MG_THINGS_HTTP_PORT}; + } + + # Proxy pass to magistrala-mqtt-adapter over WS + location /mqtt { + include snippets/verify-ssl-client.conf; + include snippets/proxy-headers.conf; + include snippets/ws-upgrade.conf; + proxy_pass http://mqtt_ws_cluster; + } + } +} + +# MQTT +stream { + include snippets/stream_access_log.conf; + + # Include JS script for mTLS + js_path "/etc/nginx/njs/"; + + js_import authorization from /etc/nginx/authorization.js; + + # Include single-node or multiple-node (cluster) upstream + include snippets/mqtt-upstream.conf; + ssl_verify_client on; + include snippets/ssl-client.conf; + + server { + listen ${MG_NGINX_MQTT_PORT}; + listen [::]:${MG_NGINX_MQTT_PORT}; + listen ${MG_NGINX_MQTTS_PORT} ssl; + listen [::]:${MG_NGINX_MQTTS_PORT} ssl; + + include snippets/ssl.conf; + js_preread authorization.authenticate; + + proxy_pass mqtt_cluster; + } +} + +error_log info.log info; diff --git a/docker/nginx/snippets/http_access_log.conf b/docker/nginx/snippets/http_access_log.conf new file mode 100644 index 0000000..3fcf5c8 --- /dev/null +++ b/docker/nginx/snippets/http_access_log.conf @@ -0,0 +1,5 @@ +log_format access_log_format 'HTTP/WS ' + '$remote_addr: ' + '"$request" $status; ' + 'request time=$request_time upstream connect time=$upstream_connect_time upstream response time=$upstream_response_time'; +access_log access.log access_log_format; diff --git a/docker/nginx/snippets/mqtt-upstream-cluster.conf b/docker/nginx/snippets/mqtt-upstream-cluster.conf new file mode 100644 index 0000000..e0f7248 --- /dev/null +++ b/docker/nginx/snippets/mqtt-upstream-cluster.conf @@ -0,0 +1,6 @@ +upstream mqtt_cluster { + least_conn; + server mqtt-adapter-1:${MG_MQTT_ADAPTER_MQTT_PORT}; + server mqtt-adapter-2:${MG_MQTT_ADAPTER_MQTT_PORT}; + server mqtt-adapter-3:${MG_MQTT_ADAPTER_MQTT_PORT}; +} \ No newline at end of file diff --git a/docker/nginx/snippets/mqtt-upstream-single.conf b/docker/nginx/snippets/mqtt-upstream-single.conf new file mode 100644 index 0000000..35a8a27 --- /dev/null +++ b/docker/nginx/snippets/mqtt-upstream-single.conf @@ -0,0 +1,3 @@ +upstream mqtt_cluster { + server mqtt-adapter:${MG_MQTT_ADAPTER_MQTT_PORT}; +} \ No newline at end of file diff --git a/docker/nginx/snippets/mqtt-ws-upstream-cluster.conf b/docker/nginx/snippets/mqtt-ws-upstream-cluster.conf new file mode 100644 index 0000000..c8ff07c --- /dev/null +++ b/docker/nginx/snippets/mqtt-ws-upstream-cluster.conf @@ -0,0 +1,6 @@ +upstream mqtt_ws_cluster { + least_conn; + server mqtt-adapter-1:${MG_MQTT_ADAPTER_WS_PORT}; + server mqtt-adapter-2:${MG_MQTT_ADAPTER_WS_PORT}; + server mqtt-adapter-3:${MG_MQTT_ADAPTER_WS_PORT}; +} \ No newline at end of file diff --git a/docker/nginx/snippets/mqtt-ws-upstream-single.conf b/docker/nginx/snippets/mqtt-ws-upstream-single.conf new file mode 100644 index 0000000..49e55bd --- /dev/null +++ b/docker/nginx/snippets/mqtt-ws-upstream-single.conf @@ -0,0 +1,3 @@ +upstream mqtt_ws_cluster { + server mqtt-adapter:${MG_MQTT_ADAPTER_WS_PORT}; +} \ No newline at end of file diff --git a/docker/nginx/snippets/proxy-headers.conf b/docker/nginx/snippets/proxy-headers.conf new file mode 100644 index 0000000..7957080 --- /dev/null +++ b/docker/nginx/snippets/proxy-headers.conf @@ -0,0 +1,12 @@ +proxy_redirect off; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; + +# Allow OPTIONS method CORS +if ($request_method = OPTIONS) { + add_header Content-Length 0; + add_header Content-Type text/plain; + return 200; +} \ No newline at end of file diff --git a/docker/nginx/snippets/ssl-client.conf b/docker/nginx/snippets/ssl-client.conf new file mode 100644 index 0000000..5fe41ce --- /dev/null +++ b/docker/nginx/snippets/ssl-client.conf @@ -0,0 +1,2 @@ +ssl_client_certificate /etc/ssl/certs/ca.crt; +ssl_verify_depth 2; diff --git a/docker/nginx/snippets/ssl.conf b/docker/nginx/snippets/ssl.conf new file mode 100644 index 0000000..2406491 --- /dev/null +++ b/docker/nginx/snippets/ssl.conf @@ -0,0 +1,13 @@ +# These paths are set to its default values as +# a volume in the docker/docker-compose.yml file. +ssl_certificate /etc/ssl/certs/magistrala-server.crt; +ssl_certificate_key /etc/ssl/private/magistrala-server.key; +ssl_dhparam /etc/ssl/certs/dhparam.pem; + +ssl_protocols TLSv1.2 TLSv1.3; +ssl_prefer_server_ciphers on; +ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; +ssl_ecdh_curve secp384r1; +ssl_session_tickets off; +resolver 8.8.8.8 8.8.4.4 valid=300s; +resolver_timeout 5s; diff --git a/docker/nginx/snippets/stream_access_log.conf b/docker/nginx/snippets/stream_access_log.conf new file mode 100644 index 0000000..b73c7f1 --- /dev/null +++ b/docker/nginx/snippets/stream_access_log.conf @@ -0,0 +1,4 @@ +log_format access_log_format '$protocol ' + '$remote_addr: ' + 'status=$status; upstream connect time=$upstream_connect_time'; +access_log access.log access_log_format; diff --git a/docker/nginx/snippets/verify-ssl-client.conf b/docker/nginx/snippets/verify-ssl-client.conf new file mode 100644 index 0000000..4ff5ec1 --- /dev/null +++ b/docker/nginx/snippets/verify-ssl-client.conf @@ -0,0 +1,6 @@ +if ($ssl_client_verify != SUCCESS) { + return 403; +} +if ($auth_key = '') { + return 403; +} \ No newline at end of file diff --git a/docker/nginx/snippets/ws-upgrade.conf b/docker/nginx/snippets/ws-upgrade.conf new file mode 100644 index 0000000..3ca7eeb --- /dev/null +++ b/docker/nginx/snippets/ws-upgrade.conf @@ -0,0 +1,6 @@ +proxy_http_version 1.1; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "Upgrade"; +proxy_connect_timeout 7d; +proxy_send_timeout 7d; +proxy_read_timeout 7d; \ No newline at end of file diff --git a/docker/spicedb/schema.zed b/docker/spicedb/schema.zed new file mode 100644 index 0000000..215797a --- /dev/null +++ b/docker/spicedb/schema.zed @@ -0,0 +1,78 @@ +definition user {} + +definition thing { + relation administrator: user + relation group: group + relation domain: domain + + permission admin = administrator + group->admin + domain->admin + permission delete = admin + permission edit = admin + group->edit + domain->edit + permission view = edit + group->view + domain->view + permission share = edit + permission publish = group + permission subscribe = group + + // These permission are made for only list purpose. It helps to list users have only particular permission excluding other higher and lower permission. + permission admin_only = admin + permission edit_only = edit - admin + permission view_only = view + + // These permission are made for only list purpose. It helps to list users from external, users who are not in group but have permission on the group through parent group + permission ext_admin = admin - administrator // For list of external admin , not having direct relation with group, but have indirect relation from parent group +} + +definition group { + relation administrator: user + relation editor: user + relation contributor: user + relation member: user + relation guest: user + + relation parent_group: group + relation domain: domain + + permission admin = administrator + parent_group->admin + domain->admin + permission delete = admin + permission edit = admin + editor + parent_group->edit + domain->edit + permission share = edit + permission view = contributor + edit + parent_group->view + domain->view + guest + permission membership = view + member + permission create = membership - guest + + // These permissions are made for listing purposes. They enable listing users who have only particular permission excluding higher-level permissions users. + permission admin_only = admin + permission edit_only = edit - admin + permission view_only = view + permission membership_only = membership - view + + // These permission are made for only list purpose. They enable listing users who have only particular permission from parent group excluding higher-level permissions. + permission ext_admin = admin - administrator // For list of external admin , not having direct relation with group, but have indirect relation from parent group + permission ext_edit = edit - editor // For list of external edit , not having direct relation with group, but have indirect relation from parent group + permission ext_view = view - contributor // For list of external view , not having direct relation with group, but have indirect relation from parent group +} + +definition domain { + relation administrator: user // combination domain + user id + relation editor: user + relation contributor: user + relation member: user + relation guest: user + + relation platform: platform + + permission admin = administrator + platform->admin + permission edit = admin + editor + permission share = edit + permission view = edit + contributor + guest + permission membership = view + member + permission create = membership - guest +} + +definition platform { + relation administrator: user + relation member: user + + permission admin = administrator + permission membership = administrator + member +} diff --git a/docker/ssl/.gitignore b/docker/ssl/.gitignore new file mode 100644 index 0000000..f054cd1 --- /dev/null +++ b/docker/ssl/.gitignore @@ -0,0 +1,4 @@ +*grpc-server* +*grpc-client* +*srl +*conf diff --git a/docker/ssl/Makefile b/docker/ssl/Makefile new file mode 100644 index 0000000..9d02520 --- /dev/null +++ b/docker/ssl/Makefile @@ -0,0 +1,167 @@ +CRT_LOCATION = certs +O = Magistrala +OU_CA = magistrala_ca +OU_CRT = magistrala_crt +EA = info@magistrala.com +CN_CA = Magistrala_Self_Signed_CA +CN_SRV = localhost +THING_SECRET = # e.g. 8f65ed04-0770-4ce4-a291-6d1bf2000f4d +CRT_FILE_NAME = thing +THINGS_GRPC_SERVER_CONF_FILE_NAME=thing-grpc-server.conf +THINGS_GRPC_CLIENT_CONF_FILE_NAME=thing-grpc-client.conf +THINGS_GRPC_SERVER_CN=things +THINGS_GRPC_CLIENT_CN=things-client +THINGS_GRPC_SERVER_CRT_FILE_NAME=things-grpc-server +THINGS_GRPC_CLIENT_CRT_FILE_NAME=things-grpc-client +AUTH_GRPC_SERVER_CONF_FILE_NAME=auth-grpc-server.conf +AUTH_GRPC_CLIENT_CONF_FILE_NAME=auth-grpc-client.conf +AUTH_GRPC_SERVER_CN=auth +AUTH_GRPC_CLIENT_CN=auth-client +AUTH_GRPC_SERVER_CRT_FILE_NAME=auth-grpc-server +AUTH_GRPC_CLIENT_CRT_FILE_NAME=auth-grpc-client + +define GRPC_CERT_CONFIG +[req] +req_extensions = v3_req +distinguished_name = dn +prompt = no + +[dn] +CN = mg.svc +C = RS +ST = RS +L = BELGRADE +O = MAGISTRALA +OU = MAGISTRALA + +[v3_req] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = <> +endef + +define ANNOUNCE_BODY +Version $(VERSION) of $(PACKAGE_NAME) has been released. + +It can be downloaded from $(DOWNLOAD_URL). + +etc, etc. +endef +all: clean_certs ca server_cert things_grpc_certs auth_grpc_certs + +# CA name and key is "ca". +ca: + openssl req -newkey rsa:2048 -x509 -nodes -sha512 -days 1095 \ + -keyout $(CRT_LOCATION)/ca.key -out $(CRT_LOCATION)/ca.crt -subj "/CN=$(CN_CA)/O=$(O)/OU=$(OU_CA)/emailAddress=$(EA)" + +# Server cert and key name is "magistrala-server". +server_cert: + # Create magistrala server key and CSR. + openssl req -new -sha256 -newkey rsa:4096 -nodes -keyout $(CRT_LOCATION)/magistrala-server.key \ + -out $(CRT_LOCATION)/magistrala-server.csr -subj "/CN=$(CN_SRV)/O=$(O)/OU=$(OU_CRT)/emailAddress=$(EA)" + + # Sign server CSR. + openssl x509 -req -days 1000 -in $(CRT_LOCATION)/magistrala-server.csr -CA $(CRT_LOCATION)/ca.crt -CAkey $(CRT_LOCATION)/ca.key -CAcreateserial -out $(CRT_LOCATION)/magistrala-server.crt + + # Remove CSR. + rm $(CRT_LOCATION)/magistrala-server.csr + +thing_cert: + # Create magistrala server key and CSR. + openssl req -new -sha256 -newkey rsa:4096 -nodes -keyout $(CRT_LOCATION)/$(CRT_FILE_NAME).key \ + -out $(CRT_LOCATION)/$(CRT_FILE_NAME).csr -subj "/CN=$(THING_SECRET)/O=$(O)/OU=$(OU_CRT)/emailAddress=$(EA)" + + # Sign client CSR. + openssl x509 -req -days 730 -in $(CRT_LOCATION)/$(CRT_FILE_NAME).csr -CA $(CRT_LOCATION)/ca.crt -CAkey $(CRT_LOCATION)/ca.key -CAcreateserial -out $(CRT_LOCATION)/$(CRT_FILE_NAME).crt + + # Remove CSR. + rm $(CRT_LOCATION)/$(CRT_FILE_NAME).csr + +things_grpc_certs: + # Things server grpc certificates + $(file > $(CRT_LOCATION)/$(THINGS_GRPC_SERVER_CRT_FILE_NAME).conf,$(subst <>,$(THINGS_GRPC_SERVER_CN),$(GRPC_CERT_CONFIG)) ) + + openssl req -new -sha256 -newkey rsa:4096 -nodes \ + -keyout $(CRT_LOCATION)/$(THINGS_GRPC_SERVER_CRT_FILE_NAME).key \ + -out $(CRT_LOCATION)/$(THINGS_GRPC_SERVER_CRT_FILE_NAME).csr \ + -config $(CRT_LOCATION)/$(THINGS_GRPC_SERVER_CRT_FILE_NAME).conf \ + -extensions v3_req + + openssl x509 -req -sha256 \ + -in $(CRT_LOCATION)/$(THINGS_GRPC_SERVER_CRT_FILE_NAME).csr \ + -CA $(CRT_LOCATION)/ca.crt \ + -CAkey $(CRT_LOCATION)/ca.key \ + -CAcreateserial \ + -out $(CRT_LOCATION)/$(THINGS_GRPC_SERVER_CRT_FILE_NAME).crt \ + -days 365 \ + -extfile $(CRT_LOCATION)/$(THINGS_GRPC_SERVER_CRT_FILE_NAME).conf \ + -extensions v3_req + + rm -rf $(CRT_LOCATION)/$(THINGS_GRPC_SERVER_CRT_FILE_NAME).csr $(CRT_LOCATION)/$(THINGS_GRPC_SERVER_CRT_FILE_NAME).conf + # Things client grpc certificates + $(file > $(CRT_LOCATION)/$(THINGS_GRPC_CLIENT_CRT_FILE_NAME).conf,$(subst <>,$(THINGS_GRPC_CLIENT_CN),$(GRPC_CERT_CONFIG)) ) + + openssl req -new -sha256 -newkey rsa:4096 -nodes \ + -keyout $(CRT_LOCATION)/$(THINGS_GRPC_CLIENT_CRT_FILE_NAME).key \ + -out $(CRT_LOCATION)/$(THINGS_GRPC_CLIENT_CRT_FILE_NAME).csr \ + -config $(CRT_LOCATION)/$(THINGS_GRPC_CLIENT_CRT_FILE_NAME).conf \ + -extensions v3_req + + openssl x509 -req -sha256 \ + -in $(CRT_LOCATION)/$(THINGS_GRPC_CLIENT_CRT_FILE_NAME).csr \ + -CA $(CRT_LOCATION)/ca.crt \ + -CAkey $(CRT_LOCATION)/ca.key \ + -CAcreateserial \ + -out $(CRT_LOCATION)/$(THINGS_GRPC_CLIENT_CRT_FILE_NAME).crt \ + -days 365 \ + -extfile $(CRT_LOCATION)/$(THINGS_GRPC_CLIENT_CRT_FILE_NAME).conf \ + -extensions v3_req + + rm -rf $(CRT_LOCATION)/$(THINGS_GRPC_CLIENT_CRT_FILE_NAME).csr $(CRT_LOCATION)/$(THINGS_GRPC_CLIENT_CRT_FILE_NAME).conf + +auth_grpc_certs: + # Auth gRPC server certificate + $(file > $(CRT_LOCATION)/$(AUTH_GRPC_SERVER_CRT_FILE_NAME).conf,$(subst <>,$(AUTH_GRPC_SERVER_CN),$(GRPC_CERT_CONFIG)) ) + + openssl req -new -sha256 -newkey rsa:4096 -nodes \ + -keyout $(CRT_LOCATION)/$(AUTH_GRPC_SERVER_CRT_FILE_NAME).key \ + -out $(CRT_LOCATION)/$(AUTH_GRPC_SERVER_CRT_FILE_NAME).csr \ + -config $(CRT_LOCATION)/$(AUTH_GRPC_SERVER_CRT_FILE_NAME).conf \ + -extensions v3_req + + openssl x509 -req -sha256 \ + -in $(CRT_LOCATION)/$(AUTH_GRPC_SERVER_CRT_FILE_NAME).csr \ + -CA $(CRT_LOCATION)/ca.crt \ + -CAkey $(CRT_LOCATION)/ca.key \ + -CAcreateserial \ + -out $(CRT_LOCATION)/$(AUTH_GRPC_SERVER_CRT_FILE_NAME).crt \ + -days 365 \ + -extfile $(CRT_LOCATION)/$(AUTH_GRPC_SERVER_CRT_FILE_NAME).conf \ + -extensions v3_req + + rm -rf $(CRT_LOCATION)/$(AUTH_GRPC_SERVER_CRT_FILE_NAME).csr $(CRT_LOCATION)/$(AUTH_GRPC_SERVER_CRT_FILE_NAME).conf + # Auth gRPC client certificate + $(file > $(CRT_LOCATION)/$(AUTH_GRPC_CLIENT_CRT_FILE_NAME).conf,$(subst <>,$(AUTH_GRPC_CLIENT_CN),$(GRPC_CERT_CONFIG)) ) + + openssl req -new -sha256 -newkey rsa:4096 -nodes \ + -keyout $(CRT_LOCATION)/$(AUTH_GRPC_CLIENT_CRT_FILE_NAME).key \ + -out $(CRT_LOCATION)/$(AUTH_GRPC_CLIENT_CRT_FILE_NAME).csr \ + -config $(CRT_LOCATION)/$(AUTH_GRPC_CLIENT_CRT_FILE_NAME).conf \ + -extensions v3_req + + openssl x509 -req -sha256 \ + -in $(CRT_LOCATION)/$(AUTH_GRPC_CLIENT_CRT_FILE_NAME).csr \ + -CA $(CRT_LOCATION)/ca.crt \ + -CAkey $(CRT_LOCATION)/ca.key \ + -CAcreateserial \ + -out $(CRT_LOCATION)/$(AUTH_GRPC_CLIENT_CRT_FILE_NAME).crt \ + -days 365 \ + -extfile $(CRT_LOCATION)/$(AUTH_GRPC_CLIENT_CRT_FILE_NAME).conf \ + -extensions v3_req + + rm -rf $(CRT_LOCATION)/$(AUTH_GRPC_CLIENT_CRT_FILE_NAME).csr $(CRT_LOCATION)/$(AUTH_GRPC_CLIENT_CRT_FILE_NAME).conf + +clean_certs: + rm -r $(CRT_LOCATION)/*.crt + rm -r $(CRT_LOCATION)/*.key diff --git a/docker/ssl/authorization.js b/docker/ssl/authorization.js new file mode 100644 index 0000000..11c7a62 --- /dev/null +++ b/docker/ssl/authorization.js @@ -0,0 +1,178 @@ +var clientKey = ''; + +// Check certificate MQTTS. +function authenticate(s) { + if (!s.variables.ssl_client_s_dn || !s.variables.ssl_client_s_dn.length || + !s.variables.ssl_client_verify || s.variables.ssl_client_verify != "SUCCESS") { + s.deny(); + return; + } + + s.on('upload', function (data) { + if (data == '') { + return; + } + + var packet_type_flags_byte = data.codePointAt(0); + // First MQTT packet contain message type and flags. CONNECT message type + // is encoded as 0001, and we're not interested in flags, so only values + // 0001xxxx (which is between 16 and 32) should be checked. + if (packet_type_flags_byte < 16 || packet_type_flags_byte >= 32) { + s.off('upload'); + s.allow(); + return; + } + + if (clientKey === '') { + clientKey = parseCert(s.variables.ssl_client_s_dn, 'CN'); + } + + var pass = parsePackage(s, data); + + if (!clientKey.length || !clientKey.endsWith(pass) ) { + s.error('Cert CN (' + clientKey + ') does not contain client password'); + s.off('upload') + s.deny(); + return; + } + + s.off('upload'); + s.allow(); + }) +} + +function parsePackage(s, data) { + // An explanation of MQTT packet structure can be found here: + // https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#msg-format. + + // CONNECT message is explained here: + // https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#connect. + + /* + 0 1 2 3 + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | TYPE | RSRVD | REMAINING LEN | PROTOCOL NAME LEN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | PROTOCOL NAME | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| + | VERSION | FLAGS | KEEP ALIVE | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| + | Payload (if any) ... | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + First byte with remaining length represents fixed header. + Remaining Length is the length of the variable header (10 bytes) plus the length of the Payload. + It is encoded in the manner described here: + http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html#_Toc442180836. + + Connect flags byte looks like this: + | 7 | 6 | 5 | 4 3 | 2 | 1 | 0 | + | Username Flag | Password Flag | Will Retain | Will QoS | Will Flag | Clean Session | Reserved | + + The payload is determined by the flags and comes in this order: + 1. Client ID (2 bytes length + ID value) + 2. Will Topic (2 bytes length + Will Topic value) if Will Flag is 1. + 3. Will Message (2 bytes length + Will Message value) if Will Flag is 1. + 4. User Name (2 bytes length + User Name value) if User Name Flag is 1. + 5. Password (2 bytes length + Password value) if Password Flag is 1. + + This method extracts Password field. + */ + + // Extract variable length header. It's 1-4 bytes. As long as continuation byte is + // 1, there are more bytes in this header. This algorithm is explained here: + // http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html#_Toc442180836 + var len_size = 1; + for (var remaining_len = 1; remaining_len < 5; remaining_len++) { + if (data.codePointAt(remaining_len) > 128) { + len_size += 1; + continue; + } + break; + } + + // CONTROL(1) + MSG_LEN(1-4) + PROTO_NAME_LEN(2) + PROTO_NAME(4) + PROTO_VERSION(1) + var flags_pos = 1 + len_size + 2 + 4 + 1; + var flags = data.codePointAt(flags_pos); + + // If there are no username and password flags (11xxxxxx), return. + if (flags < 192) { + s.error('MQTT username or password not provided'); + return ''; + } + + // FLAGS(1) + KEEP_ALIVE(2) + var shift = flags_pos + 1 + 2; + + // Number of bytes to encode length. + var len_bytes_num = 2; + + // If Wil Flag is present, Will Topic and Will Message need to be skipped as well. + var shift_flags = 196 <= flags ? 5 : 3; + var len_msb, len_lsb, len; + + for (var i = 0; i < shift_flags; i++) { + len_msb = data.codePointAt(shift).toString(16); + len_lsb = data.codePointAt(shift + 1).toString(16); + len = calcLen(len_msb, len_lsb); + shift += len_bytes_num; + if (i != shift_flags - 1) { + shift += len; + } + } + + var password = data.substring(shift, shift + len); + return password; +} + +// Check certificate HTTPS and WSS. +function setKey(r) { + if (clientKey === '') { + clientKey = parseCert(r.variables.ssl_client_s_dn, 'CN'); + } + + var auth = r.headersIn['Authorization']; + if (auth && auth.length && auth != clientKey) { + r.error('Authorization header does not match certificate'); + return ''; + } + + if (r.uri.startsWith('/ws') && (!auth || !auth.length)) { + var a; + for (a in r.args) { + if (a == 'authorization' && r.args[a] === clientKey) { + return clientKey + } + } + + r.error('Authorization param does not match certificate') + return ''; + } + + return clientKey; +} + +function calcLen(msb, lsb) { + if (lsb < 2) { + lsb = '0' + lsb; + } + + return parseInt(msb + lsb, 16); +} + +function parseCert(cert, key) { + if (cert.length) { + var pairs = cert.split(','); + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i].split('='); + if (pair[0].toUpperCase() == key) { + return "Thing " + pair[1].replace("\\", "").trim(); + } + } + } + + return ''; +} + +export default {setKey,authenticate}; diff --git a/docker/ssl/certs/ca.crt b/docker/ssl/certs/ca.crt new file mode 100644 index 0000000..34f0728 --- /dev/null +++ b/docker/ssl/certs/ca.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDyzCCArOgAwIBAgIUDIJg63dQVzoD9nmWi9YPscQwTgIwDQYJKoZIhvcNAQEN +BQAwdTEiMCAGA1UEAwwZTWFnaXN0cmFsYV9TZWxmX1NpZ25lZF9DQTETMBEGA1UE +CgwKTWFnaXN0cmFsYTEWMBQGA1UECwwNbWFnaXN0cmFsYV9jYTEiMCAGCSqGSIb3 +DQEJARYTaW5mb0BtYWdpc3RyYWxhLmNvbTAeFw0yMzEwMzAwODE5MDFaFw0yNjEw +MjkwODE5MDFaMHUxIjAgBgNVBAMMGU1hZ2lzdHJhbGFfU2VsZl9TaWduZWRfQ0Ex +EzARBgNVBAoMCk1hZ2lzdHJhbGExFjAUBgNVBAsMDW1hZ2lzdHJhbGFfY2ExIjAg +BgkqhkiG9w0BCQEWE2luZm9AbWFnaXN0cmFsYS5jb20wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCWNIeGfo/SePOvviJE6UHJhBzWcPfNVbzSF6A42WgB +DEgI3KFr+/rgWMEaCOD4QzCl3Lqa89EgCA7xCgxcqFwEo33SyhAivwoHL2pRVHXn +oee3z9U757T63YLE0qrXQY2cbyChX/OU99rZxyd5l5jUGN7MCu+RYurfTIiYN+Uv +NZdl8a3X84g7fa70EOYas7cTunWUt9x64/jYDoYmn+XPXET1yEU1dQTnKY4cRjhv +HS1u2QsadHKi1hgeILyLbB4u1T5N+WfxFknhFHTu8PVPxfowrVv/xzmxOe0zSZFd +SbhtrmwT4S1wJ4PfUa3+tYZVtjEKKbyObsAW91WzOLS9AgMBAAGjUzBRMB0GA1Ud +DgQWBBQkE4koZctEZpTz9pq6a6s6xg+myTAfBgNVHSMEGDAWgBQkE4koZctEZpTz +9pq6a6s6xg+myTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDQUAA4IBAQA7 +w/oh5U9loJsigf3X3T3jQM8PVmhsUfNMJ3kc1Yumr72S4sGKjdWwuU0vk+B3eQzh +zXAj65BHhs1pXcukeoLR7YcHABEsEMg6lar/E4A+MgAZfZFVSvPpsByIK8I5ARk+ +K1V/lWso+GJJM/lImPPnpvUWBdbntqC5WtjoMMGL9uyV3kVS6yT/kJ2ercnPzhPh +uBkL1ZH3ivDn/0JDY+T8Sfeq08vNWaTcoC7qpPwqXhuT0ytY7oaBS5wmPcvvzpZg +6zZYPZfhjhdEFYY1hDrrPYNYO72jncUnwQVp3X0DQpSvbxp681hVkcEtwHB2B8l0 +tBGhgoH+TqZs0AUjoXM0 +-----END CERTIFICATE----- diff --git a/docker/ssl/certs/ca.key b/docker/ssl/certs/ca.key new file mode 100644 index 0000000..0ba786b --- /dev/null +++ b/docker/ssl/certs/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCWNIeGfo/SePOv +viJE6UHJhBzWcPfNVbzSF6A42WgBDEgI3KFr+/rgWMEaCOD4QzCl3Lqa89EgCA7x +CgxcqFwEo33SyhAivwoHL2pRVHXnoee3z9U757T63YLE0qrXQY2cbyChX/OU99rZ +xyd5l5jUGN7MCu+RYurfTIiYN+UvNZdl8a3X84g7fa70EOYas7cTunWUt9x64/jY +DoYmn+XPXET1yEU1dQTnKY4cRjhvHS1u2QsadHKi1hgeILyLbB4u1T5N+WfxFknh +FHTu8PVPxfowrVv/xzmxOe0zSZFdSbhtrmwT4S1wJ4PfUa3+tYZVtjEKKbyObsAW +91WzOLS9AgMBAAECggEAEOxEq6jFO/WgIPgHROPR42ok1J1AMgx7nGEIjnciImIX +mJYBAtlOM+oUAYKoFBh/2eQTSyN2t4jo5AvZhjP6wBQKeE4HQN7supADRrwBF7KU +WI+MKvZpW81KrzG8CUoLsikMEFpu52UAbYJkZmznzVeq/GqsAKGYLEXjauD7S5Tu +GeGVKO4novus6t3AHnBvfalIQ1JUuJFvcd5ZDhPljlzPbbWdM4WpRPaFZIKmfXft +G7Izt58yPCYwhxohjrunRudyX3oKvmCBUOBXC8HdHzND/dLxwlrVu7OjmXprmC6P +8ggNpjAPeO8Y6+EKGne1fETNsKgODY/lXGOwECY4eQKBgQDSGi3WuoT/+DecVeSF +GfmavdGCQKOD0kdl7qCeQYAL+SPVz4157AtxZs3idapvlbrc7wvw4Ev1XT7ZmWUj +Lc4/UAITR8EkkFRVbxt2PvV86AiQtmXFguTNEX5vTszRwZ2+eqijZga5niBkqyAi +SRuTwR8WrDZau4mRNnF8bUl8dQKBgQC3BKYifRp4hHqBycHe9rSMZ8Xz+ZOy+IFA +vYap1Az+e8KuqlmD9Kfpp2Mjba1+HL5WKeTJGpFE7bhvb/xMPJgbMgtQ/cw4uDJ/ +fwv4m6arf76ebOhaZtkT1vD4NyiyB+z6xP0TRgQRr2Or98XBSvGAYDXIn5vL7fUg +KrDF0ePuKQKBgDfaOcFRiDW7uJzYwI0ZoJ8gQufLYyyR4+UXEJ/BbdbA/mPCbyuw +MkKNP8Ip4YsUVL6S1avNFKQ/i4uxGY/Gh4ORM1wIwTGFJMYpaTV/+yafUFeYBWoC +J+zT77aLTiucuuB+HwKBBtylSps4WqyCntAikK8oTLLGFAYEYRrgup5ZAoGAbQ8j +JNghxwFCs0aT9ZZTfnt0NW9auUJmWzrVHSxUVe1P1J+EWiKXUJ/DbuAzizv7nAK4 +57GiMU3rItS7pn5RMZt/rNKgOIhi5yDA9HNkPTwRTfyd9QjmgHEMBQ1xfa1FZSWv +nSWS1SsLnPU37XgIMzShuByMTVhOQs3NqwPo7AkCgYAf8AzQNjFCoTwU3SJezJ4H +9j1jvMO232hAl8UDNtqvJ1APn87tOtnfX48OMoRrP9kKI0oygE3pq7rFxu1qmTns +Zir0+KLeWGg58fSZkUEAp6kbO5CKwoeVAY9EMgd7BYBqlXLqUNfdH0L+KUOFKHha +7e82VxpgBeskzAqN1e7YRA== +-----END PRIVATE KEY----- diff --git a/docker/ssl/certs/magistrala-server.crt b/docker/ssl/certs/magistrala-server.crt new file mode 100644 index 0000000..4e893c1 --- /dev/null +++ b/docker/ssl/certs/magistrala-server.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEYjCCA0oCFGXr7rfGAynaa4KMTG1+23EEF0lYMA0GCSqGSIb3DQEBCwUAMHUx +IjAgBgNVBAMMGU1hZ2lzdHJhbGFfU2VsZl9TaWduZWRfQ0ExEzARBgNVBAoMCk1h +Z2lzdHJhbGExFjAUBgNVBAsMDW1hZ2lzdHJhbGFfY2ExIjAgBgkqhkiG9w0BCQEW +E2luZm9AbWFnaXN0cmFsYS5jb20wHhcNMjMxMDMwMDgxOTA4WhcNMjYwNzI2MDgx +OTA4WjBmMRIwEAYDVQQDDAlsb2NhbGhvc3QxEzARBgNVBAoMCk1hZ2lzdHJhbGEx +FzAVBgNVBAsMDm1hZ2lzdHJhbGFfY3J0MSIwIAYJKoZIhvcNAQkBFhNpbmZvQG1h +Z2lzdHJhbGEuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAojas +t6M294uS5q8oFmYM6DULVQ1lY3K659VusJshjGvn8bi50vhKo8PpxL6ygVpjWcHG ++/gclQnTaYZumC1TUohibpBnrFx1PZUvGiryAPudFY2nC5af5BQnYGi845FcVWx5 +FNLq+IsedgSZf7FuGcZruXiukBCWVyWJRJh+8FDakc65BPeG9FpCxbeLZ1nrDpnQ +bhHbwEQrwwHk0FHZ/3cuVFJAjwqJSivJ9598eU0YWAsqsLM3uYyvOMd8alMs5vCZ +9tMCpO2v6xTdJ6kr68SwQQAiefRy6gsD5J5A4ySyCz7KX9fHCrqx1kdcDJ/CXZmh +mXxrCFKSjqjuSn2qtm+gxvAc26Zbt5z5eihpdISDUKrjW11+yapNZLATGBX8ktek +gW467V9DQYOsbA3fNkWgd5UcV5HIViUpqFMFvi1NpWc2INi/PTDWuAIBLUiVNk0W +qMtG7/HqFRPn6MrNGpvFpglgxXGNfjsggkK/3INtFnAou2rN9+ieeuzO7Zjrtwsq +sP64GVw/vLv3tgT6TIZmDnCDCqtEGEVutt7ldu3M0/fLm4qOUsZqFGrIOO1cfI4x +7FRnHwaTsTB1Og+I7lEujb4efHV+uRjKyrGh6L6hDt94IkGm6ZEj5z/iEmq16jRX +dUbYsu4f1KlfTYdHWGHp+6kAmDn0jGCwz2BBrnsCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAKyg5kvDk+TQ6ZDCK7qxKY+uN9setYvvsLfde+Uy51a3zj8RIHRgkOT2C +LuuTtTYKu3XmfCKId0oTXynGuP+yDAIuVwuZz3S0VmA8ijoZ87LJXzsLjjTjQSzZ +ar6RmlRDH+8Bm4AOrT4TDupqifag4J0msHkNPo0jVK6fnuniqJoSlhIbbHrJTHhv +jKNXrThjr/irgg1MZ7slojieOS0QoZHRE9eunIR5enDJwB5pWUJSmZWlisI7+Ibi +06+j8wZegU0nqeWp4wFSZxKnrzz5B5Qu9SrALwlHWirzBpyr0gAcF2v7nzbWviZ/ +0VMyY4FGEbkp6trMxwJs5hGYhAiyXg== +-----END CERTIFICATE----- diff --git a/docker/ssl/certs/magistrala-server.key b/docker/ssl/certs/magistrala-server.key new file mode 100644 index 0000000..f2b56f4 --- /dev/null +++ b/docker/ssl/certs/magistrala-server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCiNqy3ozb3i5Lm +rygWZgzoNQtVDWVjcrrn1W6wmyGMa+fxuLnS+Eqjw+nEvrKBWmNZwcb7+ByVCdNp +hm6YLVNSiGJukGesXHU9lS8aKvIA+50VjacLlp/kFCdgaLzjkVxVbHkU0ur4ix52 +BJl/sW4Zxmu5eK6QEJZXJYlEmH7wUNqRzrkE94b0WkLFt4tnWesOmdBuEdvARCvD +AeTQUdn/dy5UUkCPColKK8n3n3x5TRhYCyqwsze5jK84x3xqUyzm8Jn20wKk7a/r +FN0nqSvrxLBBACJ59HLqCwPknkDjJLILPspf18cKurHWR1wMn8JdmaGZfGsIUpKO +qO5Kfaq2b6DG8Bzbplu3nPl6KGl0hINQquNbXX7Jqk1ksBMYFfyS16SBbjrtX0NB +g6xsDd82RaB3lRxXkchWJSmoUwW+LU2lZzYg2L89MNa4AgEtSJU2TRaoy0bv8eoV +E+foys0am8WmCWDFcY1+OyCCQr/cg20WcCi7as336J567M7tmOu3Cyqw/rgZXD+8 +u/e2BPpMhmYOcIMKq0QYRW623uV27czT98ubio5SxmoUasg47Vx8jjHsVGcfBpOx +MHU6D4juUS6Nvh58dX65GMrKsaHovqEO33giQabpkSPnP+ISarXqNFd1Rtiy7h/U +qV9Nh0dYYen7qQCYOfSMYLDPYEGuewIDAQABAoICACvgzTyJTkOMwipbQ+U3KpOf +UZbqnjvV23/9iEkGVX9V6vJETSOnnQ0KYBAjo0aBLDGpzIj41sZr13+KaR0J2amQ +EcwljJ2fjukfExQpfLfOV/HuFLr6Pfrkhrg57KpD9i13P5Nl8EBV5WH4IYtcc9NO +DHKpldKLYhdlpGllNKUNwenB+ONCj4NGbRxtZyyIMqCK88nqU76A0jOYLgw5r9W+ +J86QRz1KFNP231V3kyR+ubCLKLuOZuruhrE9qMZcBF/dwk/1SRhS4QyeYqopRSOr +2x9iCXFisbjkTOPI+PVYRj7rd7OQOxuIX7V+LQSPLHTEK2XItW0VZOZpBLgqoQP1 +Eu19LOOs77DI5FBia1qhSpjjVGOE6koQmCki8KSFZM+CzuflTPkWNVvTNzjKrhUj +Rbezx40VVFt+q38bsTjWJbimMSo1jChianwjtotGnGpC6pD0KnHsBmfceWaL7+eC +n9KtSeAbnXlFN/rHdK7ZeP/PTSjHa+6i1awGZxhwdVsERJy/2xwZzh3uMLS2ZhXM +Tuh1D5GzlUlkMP8K23rfaXnaOXkwYxHFGi23NmxHGSqzA3TVVreWLqRSZJd/Ar67 +9Pl4S9p9f+Xkvq8tQANfoaTbjc//dpK8rjCKnwdWA3cL7eekq9sm4+lTmik9Bn2v +Bo+3/89Fr1FvlkuQvktJAoIBAQDNuc2r/9sthHZg1hOCFd5XmnMX/mXNPs+SDPRW +/VZBHjxGApz+CoZS7qk0q7f/vzYFTB6N3778f7RsgwrZYSD4I4jumvSFNFsxsHCY +K3O4kkd2YaFaZPwUYbbAcBr6nVnW/9b1aagEfWIMQ18FHLaQ6u2OfUOcNDGZEqwj +YqJmZr8plhWLeKP2c673j6g/ztnL0w77y3LnIuLjFGex17l1lQzbUgOPSKyoQj03 +d5eRoJv2aQTaOXaBzGrDtBDDd3BpXrriJEMqSZbZFRLM28jD+VuHjfHOZRUMy1hw +vZCifRrBYA6Frko7ZweRxIkcOwQsQjV/tkzVkg9FHrVhMKQTAoIBAQDJ2r+lR73d +va1JjWoXKe5qAWtprRyI8DpJM/G2/V/V3+RVOGgBeRlu6WDiMpMd9hFB6bAmX+1y +S17svw1f4DQskkTKi9EWBsWRnh2Pnd4q91TjKFsBuci8/EtAXb7C0KV5nEtasEUJ +klMmO1evAXMhn7VzmE3Ic/ttcQHxQZ+TC4G5dGsYcideJ5zOeEIATtFypDNG/0Bw +rvmBbIIylY2KwUAx3UexRgH1hRSecTzkokT39WJbefUg952h7yZXrrhb71AfWLTC +A5MJeArqPK6z/RMxDyvnk7xW326dtBBgqYyTOIHCANRB1kAG0xEyia/WI94uyNfH +YfIHglDFGIj5AoIBAEVVNEqeXPi3Jso1+7cgtaFijR1uAFMusvfu474ZfSNPFFMn ++E7pryFuC5qTsNxBTex1HesEmDIyu9TCSTq/sEPQfgqkMHpgDcfuRdQS+NogenMc +Livv0sDvuY6beYwy0Z9S89gbtqNkulGVtwVbCvBGLK+T6eBP+tMy5s66JC9Mu2pB +iZtKmj+p9zK5uKNgjChURj138I6TRFHxg4z9PiSxifa0ajy06nN+d3ElHfDXZxih +hiAhs53FDcpM+kVWEI2CfotOW1B6IpugrYhbHgtmE4HYxcCgcnqwYWsFiCQq84Ru +YhaNibkBXRy0Vt0rypk76xnSj4x+wCS0V76cjP8CggEAHXdoaJlLdzY8OLODHDSL +0D+6zWdu9fKTn6IMlBjyx4byjxo33JcwBkfdU8fsQABuzn9trnxsbjXgepD9Q9S3 +6RXFIwg8EooUh0hcql1yVDVc1/hJKLxVOHlgBtpogYnxzgnp2ihHO7l3l+orx6lf +hDYLR/+gwzVjK7vGe9CHmfChFFCRXbU0WANSWbWmdOMMoj6kGaYjYw+37pPHgdjh +G7NQSrcxwwgkOxIdS2/eYsXpaYURwabRCOn8wenmYABqe0k5GgpaAMSCz2wNs9n9 +6tpz1cKQNzMS2F+vhygFCAdYNRmXn5l9YssC97wSE52T5J/BzHSXQ0ziBwSYA92s +CQKCAQAFPujh1HhOBtn3FOT3I2jNSTv9OJsmAeiFrhVfIw+Ij8XzzUf0aV04Et/R +/EetirP6WjNQuJ5/YYVUFWj07vSl20YP7NtDGFUlvWugJUvQByidHt5DkmehBWax +cfp5LWwZ4W/wm4F/DtPkgEXgEwY/TMXHvhvN6+JaQPO7iemWL7qsRAPea0oDLkMm +0phT3hKgcnbyewH6GU53KQgr2hUzhgGOKibAo+4ud9lY6M/X1axCepetKMl78Cz9 +rK2MgJOhDr6Nu/K2bKL8Q3zSB1n1WRNaTVnH6wY4j/FpeQvVv+qTAbZhJm7cRT5m ++C7JCqJGg66liqIMq6YyYXK//Ddl +-----END PRIVATE KEY----- diff --git a/docker/ssl/dhparam.pem b/docker/ssl/dhparam.pem new file mode 100644 index 0000000..e0f2ebb --- /dev/null +++ b/docker/ssl/dhparam.pem @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEAquN8NRcSdLOM9RiumqWH8Jw3CGVR/eQQeq+jvT3zpxlUQPAMExQb +MRCspm1oRgDWGvch3Z4zfMmBZyzKJA4BDTh4USzcE5zvnx8aUcUPZPQpwSicKgzb +QGnl0Xf/75GAWrwhxn8GNyMP29wrpcd1Qg8fEQ3HAW1fCd9girKMKY9aBaHli/h2 +R9Rd/KTbeqN88aoMjUvZHooIIZXu0A+kyulOajYQO4k3Sp6CBqv0FFcoLQnYNH13 +kMUE5qJ68U732HybTw8sofTCOxKcCfM2kVP7dVoF3prlGjUw3z3l3STY8vuTdq0B +R7PslkoQHNmqcL+2gouoWP3GI+IeRzGSSwIBAg== +-----END DH PARAMETERS----- diff --git a/docker/templates/users.tmpl b/docker/templates/users.tmpl new file mode 100644 index 0000000..642dae7 --- /dev/null +++ b/docker/templates/users.tmpl @@ -0,0 +1,13 @@ +Dear {{.User}}, + +We have received a request to reset your password for your account on {{.Host}}. To proceed with resetting your password, please click on the link below: + +{{.Content}} + +If you did not initiate this request, please disregard this message and your password will remain unchanged. + +Thank you for using {{.Host}}. + +Best regards, + +{{.Footer}} diff --git a/go.mod b/go.mod index 516a536..337baf6 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,51 @@ module github.com/absmach/propeller -go 1.22.6 +go 1.23.0 require ( + github.com/0x6flab/namegenerator v1.4.0 + github.com/absmach/magistrala v0.15.1 + github.com/eclipse/paho.mqtt.golang v1.5.0 + github.com/go-chi/chi/v5 v5.1.0 + github.com/go-kit/kit v0.13.0 github.com/google/uuid v1.6.0 - github.com/tetratelabs/wazero v1.7.3 + github.com/prometheus/client_golang v1.20.5 + github.com/spf13/cobra v1.8.1 + github.com/tetratelabs/wazero v1.8.2 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/trace v1.32.0 + golang.org/x/sync v0.10.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/sdk v1.32.0 // indirect + go.opentelemetry.io/proto/otlp v1.4.0 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 // indirect + google.golang.org/grpc v1.68.1 // indirect + google.golang.org/protobuf v1.35.2 // indirect ) diff --git a/go.sum b/go.sum index 5a87303..cc9baab 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,104 @@ +github.com/0x6flab/namegenerator v1.4.0 h1:QnkI813SZsI/hYnKD9pg3mkIlcYzCx0N4hnzb0YYME4= +github.com/0x6flab/namegenerator v1.4.0/go.mod h1:2sQzXuS6dX/KEwWtB6GJU729O3m4gBdD5oAU8hd0SyY= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/absmach/magistrala v0.15.1 h1:3Bk2hlyWcV591LxPYwlvRcyCXTfuZ1g/EkNmU+o3NNQ= +github.com/absmach/magistrala v0.15.1/go.mod h1:9pto6xuBt/IuCtZRdEha0iDQKNQ5tyNOjLXJgUiikYk= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= +github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= +github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw= -github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= +github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= +github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583 h1:v+j+5gpj0FopU0KKLDGfDo9ZRRpKdi5UBrCP0f76kuY= +google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 h1:IfdSdTcLFy4lqUQrQJLkLt1PB+AsqVz6lwkWPzWEz10= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/manager/api/doc.go b/manager/api/doc.go new file mode 100644 index 0000000..778f64e --- /dev/null +++ b/manager/api/doc.go @@ -0,0 +1 @@ +package api diff --git a/manager/api/endpoint.go b/manager/api/endpoint.go new file mode 100644 index 0000000..6f469f5 --- /dev/null +++ b/manager/api/endpoint.go @@ -0,0 +1,198 @@ +package api + +import ( + "context" + "errors" + + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/propeller/manager" + pkgerrors "github.com/absmach/propeller/pkg/errors" + "github.com/go-kit/kit/endpoint" +) + +func listPropletsEndpoint(svc manager.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req, ok := request.(listEntityReq) + if !ok { + return listpropletResponse{}, errors.Join(apiutil.ErrValidation, pkgerrors.ErrInvalidData) + } + if err := req.validate(); err != nil { + return listpropletResponse{}, errors.Join(apiutil.ErrValidation, err) + } + + proplets, err := svc.ListProplets(ctx, req.offset, req.limit) + if err != nil { + return listpropletResponse{}, err + } + + return listpropletResponse{ + PropletPage: proplets, + }, nil + } +} + +func getPropletEndpoint(svc manager.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req, ok := request.(entityReq) + if !ok { + return propletResponse{}, errors.Join(apiutil.ErrValidation, pkgerrors.ErrInvalidData) + } + if err := req.validate(); err != nil { + return propletResponse{}, errors.Join(apiutil.ErrValidation, err) + } + + node, err := svc.GetProplet(ctx, req.id) + if err != nil { + return propletResponse{}, err + } + + return propletResponse{ + Proplet: node, + }, nil + } +} + +func createTaskEndpoint(svc manager.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req, ok := request.(taskReq) + if !ok { + return taskResponse{}, errors.Join(apiutil.ErrValidation, pkgerrors.ErrInvalidData) + } + if err := req.validate(); err != nil { + return taskResponse{}, errors.Join(apiutil.ErrValidation, err) + } + + task, err := svc.CreateTask(ctx, req.Task) + if err != nil { + return taskResponse{}, err + } + + return taskResponse{ + Task: task, + created: true, + }, nil + } +} + +func listTasksEndpoint(svc manager.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req, ok := request.(listEntityReq) + if !ok { + return listTaskResponse{}, errors.Join(apiutil.ErrValidation, pkgerrors.ErrInvalidData) + } + if err := req.validate(); err != nil { + return listTaskResponse{}, errors.Join(apiutil.ErrValidation, err) + } + + tasks, err := svc.ListTasks(ctx, req.offset, req.limit) + if err != nil { + return listTaskResponse{}, err + } + + return listTaskResponse{ + TaskPage: tasks, + }, nil + } +} + +func getTaskEndpoint(svc manager.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req, ok := request.(entityReq) + if !ok { + return taskResponse{}, errors.Join(apiutil.ErrValidation, pkgerrors.ErrInvalidData) + } + if err := req.validate(); err != nil { + return taskResponse{}, errors.Join(apiutil.ErrValidation, err) + } + + task, err := svc.GetTask(ctx, req.id) + if err != nil { + return taskResponse{}, err + } + + return taskResponse{ + Task: task, + }, nil + } +} + +func updateTaskEndpoint(svc manager.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req, ok := request.(taskReq) + if !ok { + return taskResponse{}, errors.Join(apiutil.ErrValidation, pkgerrors.ErrInvalidData) + } + if err := req.validate(); err != nil { + return taskResponse{}, errors.Join(apiutil.ErrValidation, err) + } + + task, err := svc.UpdateTask(ctx, req.Task) + if err != nil { + return taskResponse{}, err + } + + return taskResponse{ + Task: task, + }, nil + } +} + +func deleteTaskEndpoint(svc manager.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req, ok := request.(entityReq) + if !ok { + return taskResponse{}, errors.Join(apiutil.ErrValidation, pkgerrors.ErrInvalidData) + } + if err := req.validate(); err != nil { + return taskResponse{}, errors.Join(apiutil.ErrValidation, err) + } + + err := svc.DeleteTask(ctx, req.id) + if err != nil { + return taskResponse{}, err + } + + return taskResponse{ + deleted: true, + }, nil + } +} + +func startTaskEndpoint(svc manager.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req, ok := request.(entityReq) + if !ok { + return messageResponse{}, errors.Join(apiutil.ErrValidation, pkgerrors.ErrInvalidData) + } + if err := req.validate(); err != nil { + return messageResponse{}, errors.Join(apiutil.ErrValidation, err) + } + + if err := svc.StartTask(ctx, req.id); err != nil { + return messageResponse{}, err + } + + return messageResponse{ + "started": true, + }, nil + } +} + +func stopTaskEndpoint(svc manager.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req, ok := request.(entityReq) + if !ok { + return messageResponse{}, errors.Join(apiutil.ErrValidation, pkgerrors.ErrInvalidData) + } + if err := req.validate(); err != nil { + return messageResponse{}, errors.Join(apiutil.ErrValidation, err) + } + if err := svc.StopTask(ctx, req.id); err != nil { + return messageResponse{}, err + } + + return messageResponse{ + "stopped": true, + }, nil + } +} diff --git a/manager/api/requests.go b/manager/api/requests.go new file mode 100644 index 0000000..d5f23c5 --- /dev/null +++ b/manager/api/requests.go @@ -0,0 +1,34 @@ +package api + +import ( + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/propeller/task" +) + +type taskReq struct { + task.Task `json:",inline"` +} + +func (t *taskReq) validate() error { + return nil +} + +type entityReq struct { + id string +} + +func (e *entityReq) validate() error { + if e.id == "" { + return apiutil.ErrMissingID + } + + return nil +} + +type listEntityReq struct { + offset, limit uint64 +} + +func (e *listEntityReq) validate() error { + return nil +} diff --git a/manager/api/responses.go b/manager/api/responses.go new file mode 100644 index 0000000..a6578de --- /dev/null +++ b/manager/api/responses.go @@ -0,0 +1,125 @@ +package api + +import ( + "net/http" + + "github.com/absmach/magistrala" + "github.com/absmach/propeller/proplet" + "github.com/absmach/propeller/task" +) + +var ( + _ magistrala.Response = (*propletResponse)(nil) + _ magistrala.Response = (*listpropletResponse)(nil) + _ magistrala.Response = (*taskResponse)(nil) + _ magistrala.Response = (*listTaskResponse)(nil) + _ magistrala.Response = (*messageResponse)(nil) +) + +type propletResponse struct { + proplet.Proplet + created bool + deleted bool +} + +func (w propletResponse) Code() int { + if w.created { + return http.StatusCreated + } + if w.deleted { + return http.StatusNoContent + } + + return http.StatusOK +} + +func (w propletResponse) Headers() map[string]string { + if w.created { + return map[string]string{ + "Location": "/tasks/" + w.ID, + } + } + + return map[string]string{} +} + +func (w propletResponse) Empty() bool { + return false +} + +type listpropletResponse struct { + proplet.PropletPage +} + +func (l listpropletResponse) Code() int { + return http.StatusOK +} + +func (l listpropletResponse) Headers() map[string]string { + return map[string]string{} +} + +func (l listpropletResponse) Empty() bool { + return false +} + +type taskResponse struct { + task.Task + created bool + deleted bool +} + +func (t taskResponse) Code() int { + if t.created { + return http.StatusCreated + } + if t.deleted { + return http.StatusNoContent + } + + return http.StatusOK +} + +func (t taskResponse) Headers() map[string]string { + if t.created { + return map[string]string{ + "Location": "/tasks/" + t.ID, + } + } + + return map[string]string{} +} + +func (t taskResponse) Empty() bool { + return false +} + +type listTaskResponse struct { + task.TaskPage +} + +func (l listTaskResponse) Code() int { + return http.StatusOK +} + +func (l listTaskResponse) Headers() map[string]string { + return map[string]string{} +} + +func (l listTaskResponse) Empty() bool { + return false +} + +type messageResponse map[string]interface{} + +func (w messageResponse) Code() int { + return http.StatusOK +} + +func (w messageResponse) Headers() map[string]string { + return map[string]string{} +} + +func (w messageResponse) Empty() bool { + return false +} diff --git a/manager/api/transport.go b/manager/api/transport.go new file mode 100644 index 0000000..19749eb --- /dev/null +++ b/manager/api/transport.go @@ -0,0 +1,146 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "log/slog" + "net/http" + "strings" + + "github.com/absmach/magistrala" + "github.com/absmach/magistrala/pkg/apiutil" + "github.com/absmach/propeller/manager" + "github.com/absmach/propeller/pkg/api" + "github.com/go-chi/chi/v5" + kithttp "github.com/go-kit/kit/transport/http" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +func MakeHandler(svc manager.Service, logger *slog.Logger, instanceID string) http.Handler { + mux := chi.NewRouter() + + opts := []kithttp.ServerOption{ + kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), + } + + mux.Route("/proplets", func(r chi.Router) { + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + listPropletsEndpoint(svc), + decodeListEntityReq, + api.EncodeResponse, + opts..., + ), "list-proplets").ServeHTTP) + r.Route("/{propletID}", func(r chi.Router) { + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + getPropletEndpoint(svc), + decodeEntityReq("propletID"), + api.EncodeResponse, + opts..., + ), "get-proplet").ServeHTTP) + }) + }) + + mux.Route("/tasks", func(r chi.Router) { + r.Post("/", otelhttp.NewHandler(kithttp.NewServer( + createTaskEndpoint(svc), + decodeTaskReq, + api.EncodeResponse, + opts..., + ), "create-task").ServeHTTP) + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + listTasksEndpoint(svc), + decodeListEntityReq, + api.EncodeResponse, + opts..., + ), "list-tasks").ServeHTTP) + r.Route("/{taskID}", func(r chi.Router) { + r.Get("/", otelhttp.NewHandler(kithttp.NewServer( + getTaskEndpoint(svc), + decodeEntityReq("taskID"), + api.EncodeResponse, + opts..., + ), "get-task").ServeHTTP) + r.Put("/", otelhttp.NewHandler(kithttp.NewServer( + updateTaskEndpoint(svc), + decodeUpdateTaskReq, + api.EncodeResponse, + opts..., + ), "update-task").ServeHTTP) + r.Delete("/", otelhttp.NewHandler(kithttp.NewServer( + deleteTaskEndpoint(svc), + decodeEntityReq("taskID"), + api.EncodeResponse, + opts..., + ), "delete-task").ServeHTTP) + r.Post("/start", otelhttp.NewHandler(kithttp.NewServer( + startTaskEndpoint(svc), + decodeEntityReq("taskID"), + api.EncodeResponse, + opts..., + ), "start-task").ServeHTTP) + r.Post("/stop", otelhttp.NewHandler(kithttp.NewServer( + stopTaskEndpoint(svc), + decodeEntityReq("taskID"), + api.EncodeResponse, + opts..., + ), "stop-task").ServeHTTP) + }) + }) + + mux.Get("/health", magistrala.Health("manager", instanceID)) + mux.Handle("/metrics", promhttp.Handler()) + + return mux +} + +func decodeEntityReq(key string) kithttp.DecodeRequestFunc { + return func(_ context.Context, r *http.Request) (interface{}, error) { + return entityReq{ + id: chi.URLParam(r, key), + }, nil + } +} + +func decodeTaskReq(_ context.Context, r *http.Request) (interface{}, error) { + if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { + return nil, errors.Join(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType) + } + var req taskReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, errors.Join(err, apiutil.ErrValidation) + } + + return req, nil +} + +func decodeUpdateTaskReq(_ context.Context, r *http.Request) (interface{}, error) { + if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) { + return nil, errors.Join(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType) + } + var req taskReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, errors.Join(err, apiutil.ErrValidation) + } + req.Task.ID = chi.URLParam(r, "taskID") + + return req, nil +} + +func decodeListEntityReq(_ context.Context, r *http.Request) (interface{}, error) { + o, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset) + if err != nil { + return nil, errors.Join(apiutil.ErrValidation, err) + } + + l, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit) + if err != nil { + return nil, errors.Join(apiutil.ErrValidation, err) + } + + return listEntityReq{ + offset: o, + limit: l, + }, nil +} diff --git a/manager/manager.go b/manager/manager.go index 3043b72..1335146 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -1,12 +1,24 @@ package manager import ( + "context" + + "github.com/absmach/propeller/proplet" "github.com/absmach/propeller/task" ) -type Manager struct { - TaskDb map[string][]task.Task - Workers []string - WorkerTaskMap map[string][]string - TaskWorkerMap map[string]string +type Service interface { + GetProplet(ctx context.Context, propletID string) (proplet.Proplet, error) + ListProplets(ctx context.Context, offset, limit uint64) (proplet.PropletPage, error) + SelectProplet(ctx context.Context, task task.Task) (proplet.Proplet, error) + + CreateTask(ctx context.Context, task task.Task) (task.Task, error) + GetTask(ctx context.Context, taskID string) (task.Task, error) + ListTasks(ctx context.Context, offset, limit uint64) (task.TaskPage, error) + UpdateTask(ctx context.Context, task task.Task) (task.Task, error) + DeleteTask(ctx context.Context, taskID string) error + StartTask(ctx context.Context, taskID string) error + StopTask(ctx context.Context, taskID string) error + + Subscribe(ctx context.Context) error } diff --git a/manager/middleware/doc.go b/manager/middleware/doc.go new file mode 100644 index 0000000..c870d7c --- /dev/null +++ b/manager/middleware/doc.go @@ -0,0 +1 @@ +package middleware diff --git a/manager/middleware/logging.go b/manager/middleware/logging.go new file mode 100644 index 0000000..b726fc8 --- /dev/null +++ b/manager/middleware/logging.go @@ -0,0 +1,244 @@ +package middleware + +import ( + "context" + "log/slog" + "time" + + "github.com/absmach/propeller/manager" + "github.com/absmach/propeller/proplet" + "github.com/absmach/propeller/task" +) + +type loggingMiddleware struct { + logger *slog.Logger + svc manager.Service +} + +func Logging(logger *slog.Logger, svc manager.Service) manager.Service { + return &loggingMiddleware{ + logger: logger, + svc: svc, + } +} + +func (lm *loggingMiddleware) GetProplet(ctx context.Context, id string) (resp proplet.Proplet, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("proplet", + slog.String("id", id), + ), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Get proplet failed", args...) + + return + } + lm.logger.Info("Get proplet completed successfully", args...) + }(time.Now()) + + return lm.svc.GetProplet(ctx, id) +} + +func (lm *loggingMiddleware) ListProplets(ctx context.Context, offset, limit uint64) (resp proplet.PropletPage, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Uint64("offset", offset), + slog.Uint64("limit", limit), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("List proplets failed", args...) + + return + } + lm.logger.Info("List proplets completed successfully", args...) + }(time.Now()) + + return lm.svc.ListProplets(ctx, offset, limit) +} + +func (lm *loggingMiddleware) SelectProplet(ctx context.Context, t task.Task) (w proplet.Proplet, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("task", + slog.String("name", t.Name), + slog.String("id", t.ID), + ), + slog.Group("proplet", + slog.String("name", w.Name), + slog.String("id", w.ID), + ), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Select proplet failed", args...) + + return + } + lm.logger.Info("Select proplet completed successfully", args...) + }(time.Now()) + + return lm.svc.SelectProplet(ctx, t) +} + +func (lm *loggingMiddleware) CreateTask(ctx context.Context, t task.Task) (resp task.Task, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("task", + slog.String("name", t.Name), + ), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Save task failed", args...) + + return + } + lm.logger.Info("Save task completed successfully", args...) + }(time.Now()) + + return lm.svc.CreateTask(ctx, t) +} + +func (lm *loggingMiddleware) GetTask(ctx context.Context, id string) (resp task.Task, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("task", + slog.String("id", id), + ), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Get task failed", args...) + + return + } + lm.logger.Info("Get task completed successfully", args...) + }(time.Now()) + + return lm.svc.GetTask(ctx, id) +} + +func (lm *loggingMiddleware) ListTasks(ctx context.Context, offset, limit uint64) (resp task.TaskPage, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Uint64("offset", offset), + slog.Uint64("limit", limit), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("List tasks failed", args...) + + return + } + lm.logger.Info("List tasks completed successfully", args...) + }(time.Now()) + + return lm.svc.ListTasks(ctx, offset, limit) +} + +func (lm *loggingMiddleware) UpdateTask(ctx context.Context, t task.Task) (resp task.Task, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("task", + slog.String("name", resp.Name), + slog.String("id", t.ID), + ), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Update task failed", args...) + + return + } + lm.logger.Info("Update task completed successfully", args...) + }(time.Now()) + + return lm.svc.UpdateTask(ctx, t) +} + +func (lm *loggingMiddleware) DeleteTask(ctx context.Context, id string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("task", + slog.String("id", id), + ), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Delete task failed", args...) + + return + } + lm.logger.Info("Delete task completed successfully", args...) + }(time.Now()) + + return lm.svc.DeleteTask(ctx, id) +} + +func (lm *loggingMiddleware) StartTask(ctx context.Context, id string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("task", + slog.String("id", id), + ), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Starting task failed", args...) + + return + } + lm.logger.Info("Starting task completed successfully", args...) + }(time.Now()) + + return lm.svc.StartTask(ctx, id) +} + +func (lm *loggingMiddleware) StopTask(ctx context.Context, id string) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("task", + slog.String("id", id), + ), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Stopping task failed", args...) + + return + } + lm.logger.Info("Stopping task completed successfully", args...) + }(time.Now()) + + return lm.svc.StopTask(ctx, id) +} + +func (lm *loggingMiddleware) Subscribe(ctx context.Context) (err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Subscribe to MQTT topic failed", args...) + + return + } + lm.logger.Info("Subscribe to MQTT topic completed successfully", args...) + }(time.Now()) + + return lm.svc.Subscribe(ctx) +} diff --git a/manager/middleware/metrics.go b/manager/middleware/metrics.go new file mode 100644 index 0000000..6b9303e --- /dev/null +++ b/manager/middleware/metrics.go @@ -0,0 +1,126 @@ +package middleware + +import ( + "context" + "time" + + "github.com/absmach/propeller/manager" + "github.com/absmach/propeller/proplet" + "github.com/absmach/propeller/task" + "github.com/go-kit/kit/metrics" +) + +var _ manager.Service = (*metricsMiddleware)(nil) + +type metricsMiddleware struct { + counter metrics.Counter + latency metrics.Histogram + svc manager.Service +} + +func Metrics(counter metrics.Counter, latency metrics.Histogram, svc manager.Service) manager.Service { + return &metricsMiddleware{ + counter: counter, + latency: latency, + svc: svc, + } +} + +func (mm *metricsMiddleware) GetProplet(ctx context.Context, id string) (proplet.Proplet, error) { + defer func(begin time.Time) { + mm.counter.With("method", "get-proplet").Add(1) + mm.latency.With("method", "get-proplet").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.GetProplet(ctx, id) +} + +func (mm *metricsMiddleware) ListProplets(ctx context.Context, offset, limit uint64) (proplet.PropletPage, error) { + defer func(begin time.Time) { + mm.counter.With("method", "list-proplets").Add(1) + mm.latency.With("method", "list-proplets").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.ListProplets(ctx, offset, limit) +} + +func (mm *metricsMiddleware) SelectProplet(ctx context.Context, t task.Task) (proplet.Proplet, error) { + defer func(begin time.Time) { + mm.counter.With("method", "select-proplet").Add(1) + mm.latency.With("method", "select-proplet").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.SelectProplet(ctx, t) +} + +func (mm *metricsMiddleware) CreateTask(ctx context.Context, t task.Task) (task.Task, error) { + defer func(begin time.Time) { + mm.counter.With("method", "create-task").Add(1) + mm.latency.With("method", "create-task").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.CreateTask(ctx, t) +} + +func (mm *metricsMiddleware) GetTask(ctx context.Context, id string) (task.Task, error) { + defer func(begin time.Time) { + mm.counter.With("method", "get-task").Add(1) + mm.latency.With("method", "get-task").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.GetTask(ctx, id) +} + +func (mm *metricsMiddleware) ListTasks(ctx context.Context, offset, limit uint64) (task.TaskPage, error) { + defer func(begin time.Time) { + mm.counter.With("method", "list-tasks").Add(1) + mm.latency.With("method", "list-tasks").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.ListTasks(ctx, offset, limit) +} + +func (mm *metricsMiddleware) UpdateTask(ctx context.Context, t task.Task) (task.Task, error) { + defer func(begin time.Time) { + mm.counter.With("method", "update-task").Add(1) + mm.latency.With("method", "update-task").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.UpdateTask(ctx, t) +} + +func (mm *metricsMiddleware) DeleteTask(ctx context.Context, id string) error { + defer func(begin time.Time) { + mm.counter.With("method", "delete-task").Add(1) + mm.latency.With("method", "delete-task").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.DeleteTask(ctx, id) +} + +func (mm *metricsMiddleware) StartTask(ctx context.Context, id string) error { + defer func(begin time.Time) { + mm.counter.With("method", "start-task").Add(1) + mm.latency.With("method", "start-task").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.StartTask(ctx, id) +} + +func (mm *metricsMiddleware) StopTask(ctx context.Context, id string) error { + defer func(begin time.Time) { + mm.counter.With("method", "stop-task").Add(1) + mm.latency.With("method", "stop-task").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.StopTask(ctx, id) +} + +func (mm *metricsMiddleware) Subscribe(ctx context.Context) error { + defer func(begin time.Time) { + mm.counter.With("method", "subscribe").Add(1) + mm.latency.With("method", "subscribe").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.Subscribe(ctx) +} diff --git a/manager/middleware/tracing.go b/manager/middleware/tracing.go new file mode 100644 index 0000000..9a3591b --- /dev/null +++ b/manager/middleware/tracing.go @@ -0,0 +1,126 @@ +package middleware + +import ( + "context" + + "github.com/absmach/propeller/manager" + "github.com/absmach/propeller/proplet" + "github.com/absmach/propeller/task" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +var _ manager.Service = (*tracing)(nil) + +type tracing struct { + tracer trace.Tracer + svc manager.Service +} + +func Tracing(tracer trace.Tracer, svc manager.Service) manager.Service { + return &tracing{tracer, svc} +} + +func (tm *tracing) GetProplet(ctx context.Context, id string) (resp proplet.Proplet, err error) { + ctx, span := tm.tracer.Start(ctx, "get-proplet", trace.WithAttributes( + attribute.String("id", id), + )) + defer span.End() + + return tm.svc.GetProplet(ctx, id) +} + +func (tm *tracing) ListProplets(ctx context.Context, offset, limit uint64) (resp proplet.PropletPage, err error) { + ctx, span := tm.tracer.Start(ctx, "list-proplets", trace.WithAttributes( + attribute.Int64("offset", int64(offset)), + attribute.Int64("limit", int64(limit)), + )) + defer span.End() + + return tm.svc.ListProplets(ctx, offset, limit) +} + +func (tm *tracing) SelectProplet(ctx context.Context, t task.Task) (resp proplet.Proplet, err error) { + ctx, span := tm.tracer.Start(ctx, "create-task", trace.WithAttributes( + attribute.String("task.name", t.Name), + attribute.String("task.id", t.ID), + attribute.String("proplet.name", resp.Name), + attribute.String("proplet.id", resp.ID), + )) + defer span.End() + + return tm.svc.SelectProplet(ctx, t) +} + +func (tm *tracing) CreateTask(ctx context.Context, t task.Task) (resp task.Task, err error) { + ctx, span := tm.tracer.Start(ctx, "create-task", trace.WithAttributes( + attribute.String("name", resp.Name), + attribute.String("id", resp.ID), + )) + defer span.End() + + return tm.svc.CreateTask(ctx, t) +} + +func (tm *tracing) GetTask(ctx context.Context, id string) (resp task.Task, err error) { + ctx, span := tm.tracer.Start(ctx, "get-task", trace.WithAttributes( + attribute.String("id", id), + )) + defer span.End() + + return tm.svc.GetTask(ctx, id) +} + +func (tm *tracing) ListTasks(ctx context.Context, offset, limit uint64) (resp task.TaskPage, err error) { + ctx, span := tm.tracer.Start(ctx, "list-tasks", trace.WithAttributes( + attribute.Int64("offset", int64(offset)), + attribute.Int64("limit", int64(limit)), + )) + defer span.End() + + return tm.svc.ListTasks(ctx, offset, limit) +} + +func (tm *tracing) UpdateTask(ctx context.Context, t task.Task) (resp task.Task, err error) { + ctx, span := tm.tracer.Start(ctx, "update-task", trace.WithAttributes( + attribute.String("id", resp.ID), + attribute.String("name", resp.Name), + )) + defer span.End() + + return tm.svc.UpdateTask(ctx, t) +} + +func (tm *tracing) DeleteTask(ctx context.Context, id string) (err error) { + ctx, span := tm.tracer.Start(ctx, "delete-task", trace.WithAttributes( + attribute.String("id", id), + )) + defer span.End() + + return tm.svc.DeleteTask(ctx, id) +} + +func (tm *tracing) StartTask(ctx context.Context, id string) (err error) { + ctx, span := tm.tracer.Start(ctx, "start-task", trace.WithAttributes( + attribute.String("id", id), + )) + defer span.End() + + return tm.svc.StartTask(ctx, id) +} + +func (tm *tracing) StopTask(ctx context.Context, id string) (err error) { + ctx, span := tm.tracer.Start(ctx, "stop-task", trace.WithAttributes( + attribute.String("id", id), + )) + defer span.End() + + return tm.svc.StopTask(ctx, id) +} + +func (tm *tracing) Subscribe(ctx context.Context) (err error) { + ctx, span := tm.tracer.Start(ctx, "subscribe") + defer span.End() + + return tm.svc.Subscribe(ctx) +} diff --git a/manager/service.go b/manager/service.go new file mode 100644 index 0000000..290bb3f --- /dev/null +++ b/manager/service.go @@ -0,0 +1,297 @@ +package manager + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/0x6flab/namegenerator" + pkgerrors "github.com/absmach/propeller/pkg/errors" + "github.com/absmach/propeller/pkg/mqtt" + "github.com/absmach/propeller/pkg/scheduler" + "github.com/absmach/propeller/pkg/storage" + "github.com/absmach/propeller/proplet" + "github.com/absmach/propeller/task" + "github.com/google/uuid" +) + +const ( + defOffset = 0 + defLimit = 100 + aliveHistoryLimit = 10 +) + +var ( + baseTopic = "channels/%s/messages" + namegen = namegenerator.NewGenerator() +) + +type service struct { + tasksDB storage.Storage + propletsDB storage.Storage + taskPropletDB storage.Storage + scheduler scheduler.Scheduler + baseTopic string + pubsub mqtt.PubSub + logger *slog.Logger +} + +func NewService( + tasksDB, propletsDB, taskPropletDB storage.Storage, + s scheduler.Scheduler, pubsub mqtt.PubSub, + channelID string, logger *slog.Logger, +) Service { + return &service{ + tasksDB: tasksDB, + propletsDB: propletsDB, + taskPropletDB: taskPropletDB, + scheduler: s, + baseTopic: fmt.Sprintf(baseTopic, channelID), + pubsub: pubsub, + logger: logger, + } +} + +func (svc *service) GetProplet(ctx context.Context, propletID string) (proplet.Proplet, error) { + data, err := svc.propletsDB.Get(ctx, propletID) + if err != nil { + return proplet.Proplet{}, err + } + w, ok := data.(proplet.Proplet) + if !ok { + return proplet.Proplet{}, pkgerrors.ErrInvalidData + } + w.SetAlive() + + return w, nil +} + +func (svc *service) ListProplets(ctx context.Context, offset, limit uint64) (proplet.PropletPage, error) { + data, total, err := svc.propletsDB.List(ctx, offset, limit) + if err != nil { + return proplet.PropletPage{}, err + } + proplets := make([]proplet.Proplet, total) + for i := range data { + w, ok := data[i].(proplet.Proplet) + if !ok { + return proplet.PropletPage{}, pkgerrors.ErrInvalidData + } + w.SetAlive() + proplets[i] = w + } + + return proplet.PropletPage{ + Offset: offset, + Limit: limit, + Total: total, + Proplets: proplets, + }, nil +} + +func (svc *service) SelectProplet(ctx context.Context, t task.Task) (proplet.Proplet, error) { + proplets, err := svc.ListProplets(ctx, defOffset, defLimit) + if err != nil { + return proplet.Proplet{}, err + } + + return svc.scheduler.SelectProplet(t, proplets.Proplets) +} + +func (svc *service) CreateTask(ctx context.Context, t task.Task) (task.Task, error) { + t.ID = uuid.NewString() + t.CreatedAt = time.Now() + + if err := svc.tasksDB.Create(ctx, t.ID, t); err != nil { + return task.Task{}, err + } + + return t, nil +} + +func (svc *service) GetTask(ctx context.Context, taskID string) (task.Task, error) { + data, err := svc.tasksDB.Get(ctx, taskID) + if err != nil { + return task.Task{}, err + } + t, ok := data.(task.Task) + if !ok { + return task.Task{}, pkgerrors.ErrInvalidData + } + + return t, nil +} + +func (svc *service) ListTasks(ctx context.Context, offset, limit uint64) (task.TaskPage, error) { + data, total, err := svc.tasksDB.List(ctx, offset, limit) + if err != nil { + return task.TaskPage{}, err + } + + tasks := make([]task.Task, total) + for i := range data { + t, ok := data[i].(task.Task) + if !ok { + return task.TaskPage{}, pkgerrors.ErrInvalidData + } + + tasks[i] = t + } + + return task.TaskPage{ + Offset: offset, + Limit: limit, + Total: total, + Tasks: tasks, + }, nil +} + +func (svc *service) UpdateTask(ctx context.Context, t task.Task) (task.Task, error) { + if err := svc.tasksDB.Update(ctx, t.ID, t); err != nil { + return task.Task{}, err + } + + return t, nil +} + +func (svc *service) DeleteTask(ctx context.Context, taskID string) error { + return svc.tasksDB.Delete(ctx, taskID) +} + +func (svc *service) StartTask(ctx context.Context, taskID string) error { + t, err := svc.GetTask(ctx, taskID) + if err != nil { + return err + } + + p, err := svc.SelectProplet(ctx, t) + if err != nil { + return err + } + + topic := "channels/" + p.ID + "/messages/control/manager/start" + if err := svc.pubsub.Publish(ctx, topic, t); err != nil { + return err + } + + if err := svc.taskPropletDB.Create(ctx, taskID, p.ID); err != nil { + return err + } + + p.TaskCount++ + if err := svc.propletsDB.Update(ctx, p.ID, p); err != nil { + return err + } + + return nil +} + +func (svc *service) StopTask(ctx context.Context, taskID string) error { + t, err := svc.GetTask(ctx, taskID) + if err != nil { + return err + } + + data, err := svc.taskPropletDB.Get(ctx, taskID) + if err != nil { + return err + } + propellerID, ok := data.(string) + if !ok { + return pkgerrors.ErrInvalidData + } + p, err := svc.GetProplet(ctx, propellerID) + if err != nil { + return err + } + + topic := "channels/" + p.ID + "/messages/control/manager/stop" + if err := svc.pubsub.Publish(ctx, topic, t); err != nil { + return err + } + + if err := svc.taskPropletDB.Delete(ctx, taskID); err != nil { + return err + } + + p.TaskCount-- + if err := svc.propletsDB.Update(ctx, p.ID, p); err != nil { + return err + } + + return nil +} + +func (svc *service) Subscribe(ctx context.Context) error { + topic := svc.baseTopic + "/#" + + if err := svc.pubsub.Subscribe(ctx, topic, svc.handle(ctx)); err != nil { + return err + } + + return nil +} + +func (svc *service) handle(ctx context.Context) func(topic string, msg map[string]interface{}) error { + return func(topic string, msg map[string]interface{}) error { + switch topic { + case svc.baseTopic + "/control/proplet/create": + if err := svc.createPropletHandler(ctx, msg); err != nil { + return err + } + svc.logger.InfoContext(ctx, "successfully created proplet") + case svc.baseTopic + "/control/proplet/alive": + return svc.updateLivenessHandler(ctx, msg) + } + + return nil + } +} + +func (svc *service) createPropletHandler(ctx context.Context, msg map[string]interface{}) error { + propletID, ok := msg["proplet_id"].(string) + if !ok { + return errors.New("invalid proplet_id") + } + if propletID == "" { + return errors.New("proplet id is empty") + } + + p := proplet.Proplet{ + ID: propletID, + Name: namegen.Generate(), + } + if err := svc.propletsDB.Create(ctx, p.ID, p); err != nil { + return err + } + + return nil +} + +func (svc *service) updateLivenessHandler(ctx context.Context, msg map[string]interface{}) error { + propletID, ok := msg["proplet_id"].(string) + if !ok { + return errors.New("invalid proplet_id") + } + if propletID == "" { + return errors.New("proplet id is empty") + } + + p, err := svc.GetProplet(ctx, propletID) + if err != nil { + return err + } + + p.Alive = true + p.AliveHistory = append(p.AliveHistory, time.Now()) + if len(p.AliveHistory) > aliveHistoryLimit { + p.AliveHistory = p.AliveHistory[1:] + } + if err := svc.propletsDB.Update(ctx, propletID, p); err != nil { + return err + } + + return nil +} diff --git a/node/node.go b/node/node.go index bb45bcb..0c39d2c 100644 --- a/node/node.go +++ b/node/node.go @@ -1,11 +1,40 @@ package node +import ( + "github.com/google/uuid" +) + +type Role uint8 + +const ( + ManagerRole Role = iota + PropletRole +) + type Node struct { - Name string - Ip string - Memory int - MemoryAllocated int - Disk int - DiskAllocated int - TaskCount int + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Memory uint64 `json:"memory"` + MemoryAllocated uint64 `json:"memory_allocated"` + Disk uint64 `json:"disk"` + DiskAllocated uint64 `json:"disk_allocated"` + TaskCount uint64 `json:"task_count"` + Role Role `json:"role"` +} + +type NodePage struct { + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Total uint64 `json:"total"` + Nodes []Node `json:"nodes"` +} + +func NewNode(name, url string, role Role) *Node { + return &Node{ + ID: uuid.NewString(), + Name: name, + URL: url, + Role: role, + } } diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 0000000..fb28f81 --- /dev/null +++ b/pkg/api/api.go @@ -0,0 +1,54 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "net/http" + + "github.com/absmach/magistrala" + pkgerrors "github.com/absmach/propeller/pkg/errors" +) + +const ( + OffsetKey = "offset" + LimitKey = "limit" + DefOffset = 0 + DefLimit = 100 + + ContentType = "application/json" + + MaxLimitSize = 100 +) + +func EncodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { + if ar, ok := response.(magistrala.Response); ok { + for k, v := range ar.Headers() { + w.Header().Set(k, v) + } + w.Header().Set("Content-Type", ContentType) + w.WriteHeader(ar.Code()) + + if ar.Empty() { + return nil + } + } + + return json.NewEncoder(w).Encode(response) +} + +func EncodeError(_ context.Context, err error, w http.ResponseWriter) { + w.Header().Set("Content-Type", ContentType) + switch { + case errors.Is(err, pkgerrors.ErrEmptyKey): + w.WriteHeader(http.StatusBadRequest) + case errors.Is(err, pkgerrors.ErrNotFound): + w.WriteHeader(http.StatusNotFound) + default: + w.WriteHeader(http.StatusInternalServerError) + } + + if err := json.NewEncoder(w).Encode(err); err != nil { + w.WriteHeader(http.StatusInternalServerError) + } +} diff --git a/pkg/doc.go b/pkg/doc.go new file mode 100644 index 0000000..c1caffe --- /dev/null +++ b/pkg/doc.go @@ -0,0 +1 @@ +package pkg diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 0000000..18fe23e --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,10 @@ +package errors + +import "errors" + +var ( + ErrNotFound = errors.New("not found") + ErrEmptyKey = errors.New("empty key") + ErrInvalidData = errors.New("invalid data type") + ErrEntityExists = errors.New("entity already exists") +) diff --git a/pkg/mqtt/pubsub.go b/pkg/mqtt/pubsub.go new file mode 100644 index 0000000..5e7bbb6 --- /dev/null +++ b/pkg/mqtt/pubsub.go @@ -0,0 +1,165 @@ +package mqtt + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "time" + + mqtt "github.com/eclipse/paho.mqtt.golang" +) + +var ( + errConnect = errors.New("failed to connect to MQTT broker") + errPublishTimeout = errors.New("failed to publish due to timeout reached") + errSubscribeTimeout = errors.New("failed to subscribe due to timeout reached") + errUnsubscribeTimeout = errors.New("failed to unsubscribe due to timeout reached") + errEmptyTopic = errors.New("empty topic") + errEmptyID = errors.New("empty ID") +) + +type pubsub struct { + address string + qos byte + id string + username string + password string + timeout time.Duration + logger *slog.Logger +} + +type Handler func(topic string, msg map[string]interface{}) error + +type PubSub interface { + Publish(ctx context.Context, topic string, msg any) error + Subscribe(ctx context.Context, topic string, handler Handler) error + Unsubscribe(ctx context.Context, topic string) error +} + +func NewPubSub(url string, qos byte, id, username, password string, timeout time.Duration, logger *slog.Logger) (PubSub, error) { + if id == "" { + return nil, errEmptyID + } + + return &pubsub{ + address: url, + qos: qos, + id: id, + username: username, + password: password, + timeout: timeout, + logger: logger, + }, nil +} + +func (ps *pubsub) Publish(ctx context.Context, topic string, msg any) error { + if topic == "" { + return errEmptyTopic + } + + client, err := newClient(ps.address, ps.id, ps.username, ps.password, ps.timeout) + if err != nil { + return err + } + data, err := json.Marshal(msg) + if err != nil { + return err + } + + token := client.Publish(topic, ps.qos, false, data) + if token.Error() != nil { + return token.Error() + } + + if ok := token.WaitTimeout(ps.timeout); !ok { + return errPublishTimeout + } + + return nil +} + +func (ps *pubsub) Subscribe(ctx context.Context, topic string, handler Handler) error { + if topic == "" { + return errEmptyTopic + } + + client, err := newClient(ps.address, ps.id, ps.username, ps.password, ps.timeout) + if err != nil { + return err + } + + token := client.Subscribe(topic, ps.qos, ps.mqttHandler(handler)) + if token.Error() != nil { + return token.Error() + } + if ok := token.WaitTimeout(ps.timeout); !ok { + return errSubscribeTimeout + } + + return nil +} + +func (ps *pubsub) Unsubscribe(ctx context.Context, topic string) error { + if topic == "" { + return errEmptyTopic + } + + client, err := newClient(ps.address, ps.id, ps.username, ps.password, ps.timeout) + if err != nil { + return err + } + + token := client.Unsubscribe(topic) + if token.Error() != nil { + return token.Error() + } + + if ok := token.WaitTimeout(ps.timeout); !ok { + return errUnsubscribeTimeout + } + + return nil +} + +func (ps *pubsub) Close() error { + return nil +} + +func newClient(address, id, username, password string, timeout time.Duration) (mqtt.Client, error) { + opts := mqtt.NewClientOptions(). + SetUsername(username). + SetPassword(password). + AddBroker(address). + SetClientID(id) + client := mqtt.NewClient(opts) + + token := client.Connect() + if token.Error() != nil { + return nil, token.Error() + } + + if ok := token.WaitTimeout(timeout); !ok { + return nil, errConnect + } + + return client, nil +} + +func (ps *pubsub) mqttHandler(h Handler) mqtt.MessageHandler { + return func(_ mqtt.Client, m mqtt.Message) { + var msg map[string]interface{} + if err := json.Unmarshal(m.Payload(), &msg); err != nil { + ps.logger.Warn(fmt.Sprintf("Failed to unmarshal received message: %s", err)) + + return + } + + if err := h(m.Topic(), msg); err != nil { + ps.logger.Warn(fmt.Sprintf("Failed to handle Magistrala message: %s", err)) + } + + m.Ack() + } +} diff --git a/pkg/scheduler/roundrobin.go b/pkg/scheduler/roundrobin.go new file mode 100644 index 0000000..d88e60c --- /dev/null +++ b/pkg/scheduler/roundrobin.go @@ -0,0 +1,46 @@ +package scheduler + +import ( + "github.com/absmach/propeller/proplet" + "github.com/absmach/propeller/task" +) + +type roundRobin struct { + LastProplet int +} + +func NewRoundRobin() Scheduler { + return &roundRobin{ + LastProplet: 0, + } +} + +func (r *roundRobin) SelectProplet(t task.Task, proplets []proplet.Proplet) (proplet.Proplet, error) { + if len(proplets) == 0 { + return proplet.Proplet{}, ErrNoProplet + } + + alive := 0 + for i := range proplets { + if proplets[i].Alive { + alive += 1 + } + } + if alive == 0 { + return proplet.Proplet{}, ErrDeadProplers + } + + if len(proplets) == 1 { + return proplets[0], nil + } + + r.LastProplet = (r.LastProplet + 1) % len(proplets) + + p := proplets[r.LastProplet] + if !p.Alive { + return r.SelectProplet(t, proplets) + } + p.TaskCount += 1 + + return p, nil +} diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go new file mode 100644 index 0000000..f8d8ddc --- /dev/null +++ b/pkg/scheduler/scheduler.go @@ -0,0 +1,17 @@ +package scheduler + +import ( + "errors" + + "github.com/absmach/propeller/proplet" + "github.com/absmach/propeller/task" +) + +var ( + ErrNoProplet = errors.New("no proplet was provided") + ErrDeadProplers = errors.New("all proplets are dead") +) + +type Scheduler interface { + SelectProplet(t task.Task, proplets []proplet.Proplet) (proplet.Proplet, error) +} diff --git a/pkg/storage/doc.go b/pkg/storage/doc.go new file mode 100644 index 0000000..82be054 --- /dev/null +++ b/pkg/storage/doc.go @@ -0,0 +1 @@ +package storage diff --git a/pkg/storage/memory.go b/pkg/storage/memory.go new file mode 100644 index 0000000..ae9a2d4 --- /dev/null +++ b/pkg/storage/memory.go @@ -0,0 +1,108 @@ +package storage + +import ( + "context" + "sync" + + "github.com/absmach/propeller/pkg/errors" +) + +type inMemoryStorage struct { + data map[string]interface{} + sync.Mutex +} + +func NewInMemoryStorage() Storage { + return &inMemoryStorage{ + data: make(map[string]interface{}), + } +} + +func (s *inMemoryStorage) Create(_ context.Context, key string, value interface{}) error { + if key == "" { + return errors.ErrEmptyKey + } + + s.Lock() + defer s.Unlock() + + if _, ok := s.data[key]; ok { + return errors.ErrEntityExists + } + + s.data[key] = value + + return nil +} + +func (s *inMemoryStorage) Get(_ context.Context, key string) (interface{}, error) { + if key == "" { + return nil, errors.ErrEmptyKey + } + + s.Lock() + defer s.Unlock() + + if val, ok := s.data[key]; ok { + return val, nil + } + + return nil, errors.ErrNotFound +} + +func (s *inMemoryStorage) Update(_ context.Context, key string, value interface{}) error { + if key == "" { + return errors.ErrEmptyKey + } + + s.Lock() + defer s.Unlock() + + if _, ok := s.data[key]; !ok { + return errors.ErrNotFound + } + + s.data[key] = value + + return nil +} + +func (s *inMemoryStorage) List(_ context.Context, offset, limit uint64) (result []interface{}, total uint64, err error) { + s.Lock() + defer s.Unlock() + + keys := make([]string, 0) + for k := range s.data { + keys = append(keys, k) + } + + total = uint64(len(keys)) + if offset >= total { + return nil, 0, nil + } + + end := offset + limit + if end > total { + end = total + } + + result = make([]interface{}, end-offset) + for i := offset; i < end; i++ { + result[i-offset] = s.data[keys[i]] + } + + return result, total, nil +} + +func (s *inMemoryStorage) Delete(_ context.Context, key string) error { + if key == "" { + return errors.ErrEmptyKey + } + + s.Lock() + defer s.Unlock() + + delete(s.data, key) + + return nil +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go new file mode 100644 index 0000000..cf85ea2 --- /dev/null +++ b/pkg/storage/storage.go @@ -0,0 +1,11 @@ +package storage + +import "context" + +type Storage interface { + Create(ctx context.Context, key string, value interface{}) error + Get(ctx context.Context, key string) (interface{}, error) + Update(ctx context.Context, key string, value interface{}) error + List(ctx context.Context, offset, limit uint64) ([]interface{}, uint64, error) + Delete(ctx context.Context, key string) error +} diff --git a/propellerd/manager.go b/propellerd/manager.go new file mode 100644 index 0000000..a435e8c --- /dev/null +++ b/propellerd/manager.go @@ -0,0 +1,126 @@ +package propellerd + +import ( + "context" + "time" + + "github.com/absmach/magistrala/pkg/server" + "github.com/absmach/propeller/cmd/manager" + "github.com/spf13/cobra" +) + +var ( + logLevel = "info" + port = "7070" + channelID = "" + thingID = "" + thingKey = "" + mqttAddress = "tcp://localhost:1883" + mqttQOS = 2 + mqttTimeout = 30 * time.Second +) + +var managerCmd = []cobra.Command{ + { + Use: "start", + Short: "Start manager", + Long: `Start manager.`, + Run: func(cmd *cobra.Command, _ []string) { + cfg := manager.Config{ + LogLevel: logLevel, + Server: server.Config{ + Port: port, + }, + ChannelID: channelID, + ThingID: thingID, + ThingKey: thingKey, + MQTTAddress: mqttAddress, + MQTTQOS: uint8(mqttQOS), + MQTTTimeout: mqttTimeout, + } + ctx, cancel := context.WithCancel(cmd.Context()) + if err := manager.StartManager(ctx, cancel, cfg); err != nil { + cmd.PrintErrf("failed to start manager: %s", err.Error()) + } + cancel() + }, + }, +} + +func NewManagerCmd() *cobra.Command { + cmd := cobra.Command{ + Use: "manager [start]", + Short: "Manager management", + Long: `Create manager for Propeller.`, + } + + for i := range managerCmd { + cmd.AddCommand(&managerCmd[i]) + } + + cmd.PersistentFlags().StringVarP( + &logLevel, + "log-level", + "l", + logLevel, + "Log level", + ) + + cmd.PersistentFlags().StringVarP( + &port, + "port", + "p", + port, + "Manager HTTP Server Port", + ) + + cmd.PersistentFlags().StringVarP( + &channelID, + "channel-id", + "c", + channelID, + "Manager Channel ID", + ) + + cmd.PersistentFlags().StringVarP( + &thingID, + "thing-id", + "t", + thingID, + "Manager Thing ID", + ) + + cmd.PersistentFlags().StringVarP( + &thingKey, + "thing-key", + "k", + thingKey, + "Thing Key", + ) + + cmd.PersistentFlags().StringVarP( + &mqttAddress, + "mqtt-address", + "m", + mqttAddress, + "MQTT Address", + ) + + cmd.PersistentFlags().IntVarP( + &mqttQOS, + "mqtt-qos", + "q", + mqttQOS, + "MQTT QOS", + ) + + cmd.PersistentFlags().DurationVarP( + &mqttTimeout, + "mqtt-timeout", + "o", + mqttTimeout, + "MQTT Timeout", + ) + + return &cmd +} diff --git a/worker/wasm.go b/proplet/wasm.go similarity index 57% rename from worker/wasm.go rename to proplet/wasm.go index e4dff64..286a10d 100644 --- a/worker/wasm.go +++ b/proplet/wasm.go @@ -1,4 +1,4 @@ -package worker +package proplet import ( "context" @@ -11,28 +11,28 @@ import ( "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" ) -var _ Worker = (*worker)(nil) +var _ Service = (*proplet)(nil) -type worker struct { +type proplet struct { mu sync.Mutex Name string - Db map[string]task.Task + DB map[string]task.Task TaskCount int runtimes map[string]wazero.Runtime functions map[string]api.Function } -func NewWasmWorker(name string) *worker { - return &worker{ +func NewWasmProplet(name string) *proplet { + return &proplet{ Name: name, - Db: make(map[string]task.Task), + DB: make(map[string]task.Task), TaskCount: 0, runtimes: make(map[string]wazero.Runtime), functions: make(map[string]api.Function), } } -func (w *worker) StartTask(ctx context.Context, task task.Task) error { +func (w *proplet) StartTask(ctx context.Context, t task.Task) error { w.mu.Lock() defer w.mu.Unlock() @@ -41,41 +41,41 @@ func (w *worker) StartTask(ctx context.Context, task task.Task) error { // implement `panic`. wasi_snapshot_preview1.MustInstantiate(ctx, r) - module, err := r.Instantiate(ctx, task.Function.File) + module, err := r.Instantiate(ctx, t.Function.File) if err != nil { return err } - function := module.ExportedFunction(task.Function.Name) + function := module.ExportedFunction(t.Function.Name) if function == nil { - return fmt.Errorf("function %q not found", task.Function.Name) + return fmt.Errorf("function %q not found", t.Function.Name) } w.TaskCount++ - w.runtimes[task.ID] = r - w.functions[task.ID] = function - w.Db[task.ID] = task + w.runtimes[t.ID] = r + w.functions[t.ID] = function + w.DB[t.ID] = t return nil } -func (w *worker) RunTask(ctx context.Context, taskID string) ([]uint64, error) { +func (w *proplet) RunTask(ctx context.Context, taskID string) ([]uint64, error) { w.mu.Lock() defer w.mu.Unlock() - task, ok := w.Db[taskID] + t, ok := w.DB[taskID] if !ok { return nil, fmt.Errorf("task %q not found", taskID) } - function := w.functions[task.ID] + function := w.functions[t.ID] - result, err := function.Call(ctx, task.Function.Inputs...) + result, err := function.Call(ctx, t.Function.Inputs...) if err != nil { return nil, err } - r := w.runtimes[task.ID] + r := w.runtimes[t.ID] if err := r.Close(ctx); err != nil { return nil, err } @@ -83,19 +83,20 @@ func (w *worker) RunTask(ctx context.Context, taskID string) ([]uint64, error) { return result, nil } -func (w *worker) StopTask(ctx context.Context, taskID string) error { +func (w *proplet) StopTask(ctx context.Context, taskID string) error { w.mu.Lock() defer w.mu.Unlock() r := w.runtimes[taskID] + return r.Close(ctx) } -func (w *worker) RemoveTask(ctx context.Context, taskID string) error { +func (w *proplet) RemoveTask(_ context.Context, taskID string) error { w.mu.Lock() defer w.mu.Unlock() - delete(w.Db, taskID) + delete(w.DB, taskID) delete(w.runtimes, taskID) delete(w.functions, taskID) w.TaskCount-- diff --git a/proplet/worker.go b/proplet/worker.go new file mode 100644 index 0000000..6a05a54 --- /dev/null +++ b/proplet/worker.go @@ -0,0 +1,44 @@ +package proplet + +import ( + "context" + "time" + + "github.com/absmach/propeller/task" +) + +const aliveTimeout = 10 * time.Second + +type Service interface { + StartTask(ctx context.Context, task task.Task) error + RunTask(ctx context.Context, taskID string) ([]uint64, error) + StopTask(ctx context.Context, taskID string) error + RemoveTask(ctx context.Context, taskID string) error +} + +type Proplet struct { + ID string `json:"id"` + Name string `json:"name"` + TaskCount uint64 `json:"task_count"` + Alive bool `json:"alive"` + AliveHistory []time.Time `json:"alive_history"` +} + +func (p *Proplet) SetAlive() { + if len(p.AliveHistory) > 0 { + lastAlive := p.AliveHistory[len(p.AliveHistory)-1] + if time.Since(lastAlive) <= aliveTimeout { + p.Alive = true + + return + } + } + p.Alive = false +} + +type PropletPage struct { + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Total uint64 `json:"total"` + Proplets []Proplet `json:"proplets"` +} diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go deleted file mode 100644 index c156ba1..0000000 --- a/scheduler/scheduler.go +++ /dev/null @@ -1,7 +0,0 @@ -package scheduler - -type Scheduler interface { - SelectCandidateNotes() - Score() - Pick() -} diff --git a/task/task.go b/task/task.go index 9058e30..0fce402 100644 --- a/task/task.go +++ b/task/task.go @@ -38,10 +38,19 @@ type Function struct { } type Task struct { - ID string - Name string - State State - Function Function - StartTime time.Time - FinishTime time.Time + ID string `json:"id"` + Name string `json:"name"` + State State `json:"state"` + Function Function `json:"function"` + StartTime time.Time `json:"start_time"` + FinishTime time.Time `json:"finish_time"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type TaskPage struct { + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Total uint64 `json:"total"` + Tasks []Task `json:"tasks"` } diff --git a/test.md b/test.md new file mode 100644 index 0000000..3df2948 --- /dev/null +++ b/test.md @@ -0,0 +1,82 @@ +# Test + +Start docker composition + +```bash +cd docker +docker compose up +``` + +Login as admin user + +```bash +USER_TOKEN=$(magistrala-cli users token admin 12345678 | jq -r .access_token) +``` + +Create a domain + +```bash +DOMAIN_ID=$(magistrala-cli domains create demo demo $USER_TOKEN | jq -r .id) +``` + +Create a thing called manager + +```bash +magistrala-cli things create '{"name": "Propeller Manager", "tags": ["manager", "propeller"], "status": "enabled"}' $DOMAIN_ID $USER_TOKEN +``` + +Set the following environment variables from the respose + +```bash +export MANAGER_THING_ID="" +export MANAGER_THING_KEY="" +``` + +Create a channel called manager + +```bash +magistrala-cli channels create '{"name": "Propeller Manager", "tags": ["manager", "propeller"], "status": "enabled"}' $DOMAIN_ID $USER_TOKEN +``` + +Set the following environment variables from the respose + +```bash +export MANAGER_CHANNEL_ID="" +``` + +Connect the thing to the manager channel + +```bash +magistrala-cli things connect $MANAGER_THING_ID $MANAGER_CHANNEL_ID $DOMAIN_ID $USER_TOKEN +``` + +Create a thing called proplet + +```bash +magistrala-cli things create '{"name": "Propeller Proplet", "tags": ["proplet", "propeller"], "status": "enabled"}' $DOMAIN_ID $USER_TOKEN +``` + +Set the following environment variables from the respose + +```bash +export PROPLET_THING_ID="" +export PROPLET_THING_KEY="" +``` + +Connect the thing to the manager channel + +```bash +magistrala-cli things connect $PROPLET_THING_ID $MANAGER_CHANNEL_ID $DOMAIN_ID $USER_TOKEN +``` + +Publish create message to the manager channel. This creates a new proplet. + +```bash +mosquitto_pub -u $PROPLET_THING_ID -P $PROPLET_THING_KEY -I propeller -t channels/$MANAGER_CHANNEL_ID/messages/control/proplet/create -h localhost -m "{\"proplet_id\": \"$PROPLET_THING_ID\", \"name\": \"proplet-1\"}" +``` + +Publish alive message to the manager channel. This updates the proplet. + +```bash +mosquitto_pub -u $PROPLET_THING_ID -P $PROPLET_THING_KEY -I propeller -t channels/$MANAGER_CHANNEL_ID/messages/control/proplet/alive -h localhost -m "{\"proplet_id\": \"$PROPLET_THING_ID\"}" +``` diff --git a/worker/worker.go b/worker/worker.go deleted file mode 100644 index 3ea4153..0000000 --- a/worker/worker.go +++ /dev/null @@ -1,14 +0,0 @@ -package worker - -import ( - "context" - - "github.com/absmach/propeller/task" -) - -type Worker interface { - StartTask(ctx context.Context, task task.Task) error - RunTask(ctx context.Context, taskID string) ([]uint64, error) - StopTask(ctx context.Context, taskID string) error - RemoveTask(ctx context.Context, taskID string) error -}