├── docs ├── src │ ├── overview.md │ ├── datatypes.md │ ├── datsatypes.md │ ├── messaging-patterns.broadcast.md │ ├── messaging-patterns.unicast.md │ ├── messaging-patterns.multicast.md │ ├── messaging-patterns.md │ ├── message-properties.md │ ├── datatypes.account_id.md │ ├── datatypes.agent_id.md │ ├── datatypes.session_id.md │ ├── datatypes.tracking_id.md │ ├── SUMMARY.md │ ├── messaging-patterns.broadcast.event.md │ ├── messaging-patterns.multicast.event.md │ ├── messaging-patterns.unicast.response.md │ ├── messaging-patterns.unicast.request.md │ ├── messaging-patterns.multicast.request.md │ ├── message-properties.broker.md │ └── message-properties.application.md └── book.toml ├── rel ├── sys.config └── vm.args ├── elvis ├── .dockerignore ├── chart ├── templates │ ├── rbac │ │ ├── sa.yaml │ │ ├── role.yaml │ │ └── rolebinding.yaml │ ├── headless-svc.yaml │ ├── pdb.yaml │ ├── util │ │ └── servicemonitor.yaml │ ├── network │ │ ├── external-svc.yaml │ │ ├── internal-svc.yaml │ │ ├── ing.yaml │ │ └── cluster-svc.yaml │ ├── _helpers.tpl │ ├── sts.yaml │ └── app-cm.yaml ├── .helmignore ├── Chart.yaml └── values.yaml ├── relx.config ├── data └── keys │ ├── iam.public_key.pem.sample │ ├── svc.public_key.pem.sample │ ├── iam.private_key.pem.sample │ └── svc.private_key.pem.sample ├── .editorconfig ├── .gitignore ├── src ├── mqttgw_http.erl ├── mqttgw_stat.erl ├── mqttgw_app.erl ├── mqttgw_config.erl ├── mqttgw_sup.erl ├── mqttgw_state.erl ├── mqttgw_ratelimit.erl ├── mqttgw_dynsub.erl ├── mqttgw_broker.erl ├── mqttgw_ratelimitstate.erl ├── mqttgw_authz.erl ├── mqttgw_id.erl ├── mqttgw_http_subscription.erl ├── mqttgw_authn.erl ├── mqttgw_dyn_srv.erl └── mqttgw.erl ├── elvis.config ├── App.toml.sample ├── skaffold.yaml ├── .github └── workflows │ ├── build-dev-artifacts.yml │ ├── build-release-artifacts.yml │ ├── docs.yml │ └── deploy.yml ├── docker ├── Dockerfile └── vernemq.conf ├── .travis.yml ├── LICENSE ├── Makefile ├── deploy.init.sh └── README.md /docs/src/overview.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/datatypes.md: -------------------------------------------------------------------------------- 1 | # Data Types 2 | -------------------------------------------------------------------------------- /docs/src/datsatypes.md: -------------------------------------------------------------------------------- 1 | # Data Types 2 | -------------------------------------------------------------------------------- /rel/sys.config: -------------------------------------------------------------------------------- 1 | [ 2 | {mqttgw, [ 3 | ]} 4 | ]. 5 | 6 | -------------------------------------------------------------------------------- /elvis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxford/mqtt-gateway/HEAD/elvis -------------------------------------------------------------------------------- /rel/vm.args: -------------------------------------------------------------------------------- 1 | -name mqttgw@127.0.0.1 2 | -setcookie mqttgw 3 | -heart 4 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = [] 3 | multilingual = false 4 | src = "src" 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !Makefile 3 | !erlang.mk 4 | !relx.config 5 | !rel 6 | !src 7 | !docker/entrypoint.sh 8 | !docker/vernemq.conf 9 | -------------------------------------------------------------------------------- /chart/templates/rbac/sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | -------------------------------------------------------------------------------- /relx.config: -------------------------------------------------------------------------------- 1 | {release, {mqttgw, "1"}, [mqttgw]}. 2 | {extended_start_script, true}. 3 | {sys_config, "rel/sys.config"}. 4 | {vm_args, "rel/vm.args"}. 5 | 6 | -------------------------------------------------------------------------------- /docs/src/messaging-patterns.broadcast.md: -------------------------------------------------------------------------------- 1 | ## Broadcast 2 | 3 | Always an [event](messaging-patterns.broadcast.event.md) that doesn't have any particular recipient and may be processed by any agent. -------------------------------------------------------------------------------- /data/keys/iam.public_key.pem.sample: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWWwNQde0SgLFERWZ1EX+1yKBQHWj 3 | DoYc0yVOerzcnh5BmSi2YXtM4OUmbBoTZM7atDBOmG9iVnSnr+vY1EYPfA== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /data/keys/svc.public_key.pem.sample: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWWwNQde0SgLFERWZ1EX+1yKBQHWj 3 | DoYc0yVOerzcnh5BmSi2YXtM4OUmbBoTZM7atDBOmG9iVnSnr+vY1EYPfA== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /docs/src/messaging-patterns.unicast.md: -------------------------------------------------------------------------------- 1 | # Unicast 2 | 3 | A [request](messaging-patterns.unicast.request.md) or a [response](messaging-patterns.unicast.response.md) that must be processed by the particular application's agent. 4 | -------------------------------------------------------------------------------- /docs/src/messaging-patterns.multicast.md: -------------------------------------------------------------------------------- 1 | # Multicast 2 | 3 | An [event](messaging-patterns.multicast.event.md) or a [request](messaging-patterns.multicast.request.md) that may be processed by any agent of the particular application. 4 | -------------------------------------------------------------------------------- /docs/src/messaging-patterns.md: -------------------------------------------------------------------------------- 1 | # Messaging Patterns 2 | 3 | The MQTT Gateway supports three messaging patterns: 4 | - [Broadcast](messaging-patterns.broadcast.md) 5 | - [Multicast](messaging-patterns.multicast.md) 6 | - [Unicast](messaging-patterns.unicast.md) 7 | -------------------------------------------------------------------------------- /data/keys/iam.private_key.pem.sample: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIGrdZDgyouytRYS7lPJTEEnovv+YEPBSFEjK2HMBI0FHoAoGCCqGSM49 3 | AwEHoUQDQgAEWWwNQde0SgLFERWZ1EX+1yKBQHWjDoYc0yVOerzcnh5BmSi2YXtM 4 | 4OUmbBoTZM7atDBOmG9iVnSnr+vY1EYPfA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /data/keys/svc.private_key.pem.sample: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIGrdZDgyouytRYS7lPJTEEnovv+YEPBSFEjK2HMBI0FHoAoGCCqGSM49 3 | AwEHoUQDQgAEWWwNQde0SgLFERWZ1EX+1yKBQHWjDoYc0yVOerzcnh5BmSi2YXtM 4 | 4OUmbBoTZM7atDBOmG9iVnSnr+vY1EYPfA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [**.{yml,yaml}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /chart/templates/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | kind: Role 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | rules: 6 | - apiGroups: [""] # "" indicates the core API group 7 | resources: ["pods"] 8 | verbs: ["get", "watch", "list"] 9 | -------------------------------------------------------------------------------- /chart/templates/headless-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }}-headless 5 | labels: 6 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 7 | spec: 8 | selector: 9 | {{- include "mqtt-gateway.selectorLabels" . | nindent 4 }} 10 | clusterIP: None 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | .DS_Store 3 | ._* 4 | .Spotlight-V100 5 | .Trashes 6 | 7 | # Vim 8 | .*.sw[a-z] 9 | *.un~ 10 | Session.vim 11 | 12 | # Erlang 13 | deps 14 | logs 15 | ebin 16 | doc 17 | log 18 | _rel 19 | relx 20 | erl_crash.dump 21 | .erlang.mk 22 | *.beam 23 | *.plt 24 | *.d 25 | 26 | # Project 27 | App.toml 28 | deploy 29 | /docs/book 30 | .vscode 31 | -------------------------------------------------------------------------------- /chart/templates/pdb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1beta1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | labels: 6 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 7 | spec: 8 | minAvailable: 1 9 | selector: 10 | matchLabels: 11 | {{- include "mqtt-gateway.selectorLabels" . | nindent 6 }} 12 | -------------------------------------------------------------------------------- /chart/templates/rbac/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | kind: RoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | subjects: 6 | - kind: ServiceAccount 7 | name: {{ include "mqtt-gateway.fullname" . }} 8 | roleRef: 9 | kind: Role 10 | name: {{ include "mqtt-gateway.fullname" . }} 11 | apiGroup: rbac.authorization.k8s.io 12 | -------------------------------------------------------------------------------- /docs/src/message-properties.md: -------------------------------------------------------------------------------- 1 | # Message properties 2 | 3 | Message properties may be logically separated into the three category: 4 | - [required by the messaging pattern and the message type](messaging-patterns.md). 5 | - [set by the broker agent](message-properties.broker.md) transmitting the message. 6 | - [set by the application agent](message-properties.broker.md) responding or forwarding the message. 7 | -------------------------------------------------------------------------------- /src/mqttgw_http.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw_http). 2 | -export([start/1, stop/0]). 3 | 4 | start(AuthnConfig) -> 5 | Dispatch = cowboy_router:compile([ 6 | {'_', [ 7 | {"/api/v1/subscriptions", mqttgw_http_subscription, AuthnConfig} 8 | ]} 9 | ]), 10 | {ok, _} = cowboy:start_clear(mqttgw_http, [{port, 8081}], #{ 11 | env => #{dispatch => Dispatch} 12 | }), 13 | ok. 14 | 15 | stop() -> 16 | ok = cowboy:stop_listener(mqttgw_http). 17 | -------------------------------------------------------------------------------- /chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /elvis.config: -------------------------------------------------------------------------------- 1 | [ 2 | {elvis, [ 3 | {config, [ 4 | #{dirs => ["src"], 5 | filter => "*.erl", 6 | rules => [{elvis_style, dont_repeat_yourself, #{min_complexity => 35}}], 7 | ruleset => erl_files}, 8 | #{dirs => ["."], 9 | filter => "Makefile", 10 | ruleset => makefiles}, 11 | #{dirs => ["."], 12 | filter => "elvis.config", 13 | ruleset => elvis_config} 14 | ]} 15 | ]} 16 | ]. 17 | -------------------------------------------------------------------------------- /App.toml.sample: -------------------------------------------------------------------------------- 1 | id = "mqtt-gateway.svc.example.org" 2 | agent_label = "alpha" 3 | 4 | [rate_limit] 5 | message_count = 10 6 | byte_count = 10240 7 | 8 | [authn."iam.svc.example.net"] 9 | audience = ["usr.example.net"] 10 | algorithm = "ES256" 11 | key = "/app/data/keys/iam.public_key.pem.sample" 12 | 13 | [authn."svc.example.org"] 14 | audience = ["svc.example.org"] 15 | algorithm = "ES256" 16 | key = "/app/data/keys/svc.public_key.pem.sample" 17 | 18 | [authz."svc.example.org"] 19 | type = "local" 20 | trusted = ["app.svc.example.org", "devops.svc.example.org"] 21 | -------------------------------------------------------------------------------- /src/mqttgw_stat.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw_stat). 2 | 3 | %% API 4 | -export([ 5 | read_config/0 6 | ]). 7 | 8 | %% ============================================================================= 9 | %% API 10 | %% ============================================================================= 11 | 12 | -spec read_config() -> disabled | enabled. 13 | read_config() -> 14 | case os:getenv("APP_STAT_ENABLED", "1") of 15 | "0" -> 16 | error_logger:info_msg("[CONFIG] Stat is disabled~n"), 17 | disabled; 18 | _ -> 19 | enabled 20 | end. 21 | -------------------------------------------------------------------------------- /src/mqttgw_app.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([ 7 | start/2, 8 | stop/1 9 | ]). 10 | 11 | %% ============================================================================= 12 | %% Application callbacks 13 | %% ============================================================================= 14 | 15 | start(_StartType, _StartArgs) -> 16 | mqttgw_state:new(), 17 | mqttgw_ratelimitstate:new(), 18 | ok = mqttgw_http:start(mqttgw_authn:read_config()), 19 | mqttgw_sup:start_link(). 20 | 21 | stop(_State) -> 22 | ok. 23 | -------------------------------------------------------------------------------- /docs/src/datatypes.account_id.md: -------------------------------------------------------------------------------- 1 | # AccountId 2 | 3 | `AccountId` is a compound FQDN like identifier of an account. 4 | 5 | ```bash 6 | ${ACCOUNT_LABEL}.${AUDIENCE} 7 | ``` 8 | 9 | Name | Type | Default | Description 10 | ------------------ | ------- | ---------- | ------------------ 11 | ACCOUNT_LABEL | String | _required_ | An opaque tenant specific account label. 12 | AUDIENCE | String | _required_ | An audience of the tenant. 13 | 14 | 15 | 16 | **Example** 17 | 18 | ```bash 19 | john-doe.usr.example.net 20 | ``` 21 | Where 22 | - `john-doe` is the account label. 23 | - `usr.example.net` is the audience. 24 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta11 2 | kind: Config 3 | build: 4 | artifacts: 5 | - image: cr.yandex/crp1of6bddata8ain3q5/mqtt-gateway 6 | docker: 7 | dockerfile: docker/Dockerfile 8 | tagPolicy: 9 | gitCommit: {} 10 | local: 11 | push: true 12 | useDockerCLI: true 13 | tryImportMissing: true 14 | deploy: 15 | helm: 16 | releases: 17 | - name: mqtt-gateway 18 | chartPath: chart 19 | artifactOverrides: 20 | app.image: cr.yandex/crp1of6bddata8ain3q5/mqtt-gateway 21 | imageStrategy: 22 | helm: {} 23 | valuesFiles: 24 | - deploy/values.yaml 25 | -------------------------------------------------------------------------------- /chart/templates/util/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | labels: 6 | {{- toYaml .Values.serviceMonitor.labels | nindent 4 }} 7 | spec: 8 | endpoints: 9 | - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 10 | honorLabels: true 11 | interval: 30s 12 | port: metrics 13 | scheme: http 14 | tlsConfig: 15 | insecureSkipVerify: true 16 | jobLabel: {{ include "mqtt-gateway.name" . }} 17 | selector: 18 | matchLabels: 19 | {{- include "mqtt-gateway.selectorLabels" . | nindent 6 }} 20 | -------------------------------------------------------------------------------- /docs/src/datatypes.agent_id.md: -------------------------------------------------------------------------------- 1 | # AgentId 2 | 3 | `AgentId` is a compound FQDN like identifier of an agent. 4 | 5 | ```bash 6 | ${AGENT_LABEL}.${ACCOUNT_ID} 7 | ``` 8 | 9 | Name | Type | Default | Description 10 | ---------------- | --------- | ---------- | ------------------ 11 | AGENT_LABEL | String | _required_ | A label describing a particular agent of the account, like web or mobile. 12 | ACCOUNT_ID | AccountId | _required_ | An account identifier of the agent. 13 | 14 | 15 | 16 | **Example** 17 | 18 | ```bash 19 | web.john-doe.usr.example.net 20 | ``` 21 | Where 22 | - `web` is the agent label. 23 | - `john-doe.usr.example.net` is the account ientifier. 24 | -------------------------------------------------------------------------------- /.github/workflows/build-dev-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Build pre-release artifacts 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - main 7 | - master 8 | 9 | jobs: 10 | build-chart: 11 | uses: foxford/reusable-workflows/.github/workflows/build-pre-release-chart.yml@master 12 | secrets: 13 | helm_registry_username: ${{ secrets.YANDEX_HELM_USERNAME }} 14 | helm_registry_password: ${{ secrets.YANDEX_HELM_PASSWORD }} 15 | 16 | build-image: 17 | uses: foxford/reusable-workflows/.github/workflows/build-pre-release-image.yml@master 18 | secrets: 19 | docker_registry_username: ${{ secrets.YANDEX_DOCKER_USERNAME }} 20 | docker_registry_password: ${{ secrets.YANDEX_DOCKER_PASSWORD }} 21 | -------------------------------------------------------------------------------- /src/mqttgw_config.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw_config). 2 | 3 | %% API 4 | -export([ 5 | read_config_file/0, 6 | read_config_file/1 7 | ]). 8 | 9 | %% ============================================================================= 10 | %% API 11 | %% ============================================================================= 12 | 13 | -spec read_config_file() -> toml:config(). 14 | read_config_file() -> 15 | case os:getenv("APP_CONFIG") of 16 | false -> exit(missing_config_path); 17 | Path -> read_config_file(Path) 18 | end. 19 | 20 | -spec read_config_file(list()) -> toml:config(). 21 | read_config_file(Path) -> 22 | case toml:read_file(Path) of 23 | {ok, Config} -> Config; 24 | {error, Reason} -> exit({invalid_config_path, Reason}) 25 | end. 26 | -------------------------------------------------------------------------------- /docs/src/datatypes.session_id.md: -------------------------------------------------------------------------------- 1 | # SessionId 2 | 3 | `SessionId` is a compound FQDN like identifier of an agent session. 4 | 5 | ```bash 6 | ${AGENT_SESSION_LABEL}.${BROKER_SESSION_LABEL} 7 | ``` 8 | 9 | Name | Type | Default | Description 10 | -------------------- | ------- | ---------- | ------------------ 11 | AGENT_SESSION_LABEL | Uuid | _required_ | A session label of the agent. 12 | BROKER_SESSION_LABEL | Uuid | _required_ | A session label of the broker. 13 | 14 | 15 | 16 | **Example** 17 | 18 | ```bash 19 | 123e4567-e89b-12d3-a456-426655440000.00112233-4455-6677-8899-aabbccddeeff 20 | ``` 21 | Where 22 | - `123e4567-e89b-12d3-a456-426655440000` is the session label of the agent. 23 | - `00112233-4455-6677-8899-aabbccddeeff` is the session label of the broker. 24 | -------------------------------------------------------------------------------- /docs/src/datatypes.tracking_id.md: -------------------------------------------------------------------------------- 1 | # TrackingId 2 | 3 | `TrackingId` is a compound FQDN like identifier of a message series. 4 | 5 | ```bash 6 | ${TRACKING_LABEL}.${SESSION_ID} 7 | ``` 8 | 9 | Name | Type | Default | Description 10 | --------------- | --------- | ---------- | ------------------ 11 | TRACKING_LABEL | Uuid | _required_ | An identifier of the initial message of the series. 12 | SESSION_ID | SessionId | _required_ | A session id. 13 | 14 | 15 | 16 | **Example** 17 | 18 | ```bash 19 | 1b671a64-40d5-491e-99b0-da01ff1f3341.123e4567-e89b-12d3-a456-426655440000.00112233-4455-6677-8899-aabbccddeeff 20 | ``` 21 | Where 22 | - `1b671a64-40d5-491e-99b0-da01ff1f3341` is the tracking label. 23 | - `123e4567-e89b-12d3-a456-426655440000.00112233-4455-6677-8899-aabbccddeeff` is the session id. 24 | -------------------------------------------------------------------------------- /src/mqttgw_sup.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw_sup). 2 | -behaviour(supervisor). 3 | 4 | %% API 5 | -export([ 6 | start_link/0 7 | ]). 8 | 9 | %% Supervisor callbacks 10 | -export([ 11 | init/1 12 | ]). 13 | 14 | %% ============================================================================= 15 | %% API 16 | %% ============================================================================= 17 | 18 | start_link() -> 19 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 20 | 21 | %% ============================================================================= 22 | %% Supervisor callbacks 23 | %% ============================================================================= 24 | 25 | init([]) -> 26 | Flags = #{}, 27 | Procs = [#{ 28 | id => mqttgw_dyn_srv, 29 | start => {mqttgw_dyn_srv, start, []} 30 | }], 31 | {ok, {Flags, Procs}}. 32 | -------------------------------------------------------------------------------- /.github/workflows/build-release-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Build release version of charts and images and push into registry 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | tags: 9 | - '*.*.*' 10 | 11 | jobs: 12 | build-chart: 13 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' 14 | uses: foxford/reusable-workflows/.github/workflows/build-release-chart.yml@master 15 | secrets: 16 | helm_registry_username: ${{ secrets.YANDEX_HELM_USERNAME }} 17 | helm_registry_password: ${{ secrets.YANDEX_HELM_PASSWORD }} 18 | 19 | build-image: 20 | if: startsWith(github.ref, 'refs/tags/') 21 | uses: foxford/reusable-workflows/.github/workflows/build-release-image.yml@master 22 | secrets: 23 | docker_registry_username: ${{ secrets.YANDEX_DOCKER_USERNAME }} 24 | docker_registry_password: ${{ secrets.YANDEX_DOCKER_PASSWORD }} 25 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Overview](overview.md) 4 | - [Message properties](message-properties.md) 5 | - [Set by the broker](message-properties.broker.md) 6 | - [Set by the application](message-properties.application.md) 7 | - [Messaging patterns](messaging-patterns.md) 8 | - [Broadcast](messaging-patterns.broadcast.md) 9 | - [Event](messaging-patterns.broadcast.event.md) 10 | - [Multicast](messaging-patterns.multicast.md) 11 | - [Event](messaging-patterns.multicast.event.md) 12 | - [Request](messaging-patterns.multicast.request.md) 13 | - [Unicast](messaging-patterns.unicast.md) 14 | - [Request](messaging-patterns.unicast.request.md) 15 | - [Response](messaging-patterns.unicast.response.md) 16 | - [Data Types](datatypes.md) 17 | - [AccountId](datatypes.account_id.md) 18 | - [AgentId](datatypes.agent_id.md) 19 | - [SessionId](datatypes.session_id.md) 20 | - [TrackingId](datatypes.tracking_id.md) 21 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ## ----------------------------------------------------------------------------- 2 | ## Build 3 | ## ----------------------------------------------------------------------------- 4 | FROM erlang:21-slim AS build-stage 5 | 6 | RUN apt update && apt install -y --no-install-recommends \ 7 | ca-certificates \ 8 | build-essential \ 9 | libssl-dev \ 10 | git \ 11 | curl \ 12 | perl 13 | 14 | WORKDIR "/build" 15 | COPY . . 16 | RUN make app && make rel 17 | 18 | ## ----------------------------------------------------------------------------- 19 | ## App 20 | ## ----------------------------------------------------------------------------- 21 | FROM cr.yandex/crp1of6bddata8ain3q5/vernemq:1.11.0-buster.1 22 | 23 | COPY --from=build-stage "/build/_rel/mqttgw" "/app/mqttgw/" 24 | COPY "docker/vernemq.conf" "/vernemq/etc/vernemq.conf" 25 | 26 | RUN useradd -m vernemq && chown -R vernemq:vernemq /vernemq/data /vernemq/etc /vernemq/log 27 | 28 | USER vernemq 29 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Install awscli 15 | run: sudo apt update && sudo apt install -y awscli 16 | - name: Install mdbook 17 | run : curl -fsSL https://github.com/rust-lang/mdBook/releases/download/v0.4.10/mdbook-v0.4.10-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory $HOME 18 | - name: Upload mdbook 19 | run: ./deploy.init.sh && $HOME/mdbook build docs && ./deploy/ci-mdbook.sh 20 | env: 21 | GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} 22 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 23 | AWS_ENDPOINT: ${{ secrets.AWS_ENDPOINT }} 24 | AWS_REGION: ${{ secrets.AWS_REGION }} 25 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 26 | MDBOOK_BUCKET: docs.netology-group.services.website.yandexcloud.net 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 20.3 4 | 5 | addons: 6 | apt: 7 | packages: 8 | - awscli 9 | 10 | services: 11 | - docker 12 | 13 | git: 14 | depth: 1 15 | 16 | jobs: 17 | include: 18 | - stage: check 19 | name: Tests 20 | install: make elvis deps plt 21 | script: make check EUNIT_OPTS=verbose 22 | - stage: build 23 | name: Docs 24 | install: 25 | - curl https://sh.rustup.rs -sSf | sh -s -- -y 26 | - ${HOME}/.cargo/bin/cargo install mdbook --vers ^0.4 27 | script: 28 | - ${TRAVIS_HOME}/.cargo/bin/mdbook build docs 29 | - ./deploy.init.sh 30 | - ./deploy/ci-mdbook.sh 31 | - stage: build 32 | name: Build 33 | script: 34 | - ./deploy.init.sh 35 | - ./deploy/ci-install-tools.sh 36 | - ./deploy/ci-build.sh 37 | 38 | stages: 39 | - name: check 40 | - name: build 41 | if: branch = master AND type = push 42 | 43 | notifications: 44 | email: false 45 | -------------------------------------------------------------------------------- /docs/src/messaging-patterns.broadcast.event.md: -------------------------------------------------------------------------------- 1 | ## Broadcast event 2 | 3 | An event that doesn't have any patricular recipient and may be processed by any agent. 4 | 5 | **Topic** 6 | 7 | ```bash 8 | apps/${SENDER_ACCOUNT_ID}/api/${SENDER_VERSION}/${OBJECT_PATH} 9 | ``` 10 | 11 | **Topic parameters** 12 | 13 | Name | Type | Default | Description 14 | ----------------- | --------- | ---------- | ------------------ 15 | SENDER_ACCOUNT_ID | AccountId | _required_ | An account identifier of the application sending the event. 16 | SENDER_VERSION | String | _required_ | A version of an application sending the event. 17 | OBJECT_PATH | String | _required_ | The event relates to this object. 18 | 19 | **Properties** 20 | 21 | Name | Type | Default | Description 22 | ------------------ | ------- | ---------- | ------------------ 23 | type | String | _required_ | Always `event`. 24 | label | String | _required_ | A label describing the event. 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | cluster: 7 | description: 'Select cluster' 8 | required: false 9 | default: 'dev' 10 | namespace: 11 | type: choice 12 | description: 'Select namespace' 13 | options: 14 | - s01-classroom-foxford 15 | - s01-minigroup-foxford 16 | - s01-minigroup-b2g 17 | - s01-webinar-foxford 18 | - s01-webinar-b2g 19 | - s01-webinar-tt 20 | - t01 21 | - t02 22 | - t03 23 | version: 24 | description: 'Commit/tag/branch' 25 | required: false 26 | default: 'master' 27 | 28 | jobs: 29 | deploy: 30 | uses: foxford/reusable-workflows/.github/workflows/deploy-via-flux.yml@master 31 | with: 32 | cluster: ${{ inputs.cluster }} 33 | namespace: ${{ inputs.namespace }} 34 | version: ${{ inputs.version }} 35 | secrets: 36 | gh_token: ${{ secrets._GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /docs/src/messaging-patterns.multicast.event.md: -------------------------------------------------------------------------------- 1 | ## Multicast event 2 | 3 | An event that may be processed by any agent of the particular application. 4 | 5 | **Topic** 6 | 7 | ```bash 8 | agents/${SENDER_AGENT_ID}/api/${RECIPIENT_VERSION}/out/${RECIPIENT_ACCOUNT_ID} 9 | ``` 10 | 11 | **Topic parameters** 12 | 13 | Name | Type | Default | Description 14 | -------------------- | --------- | ---------- | ------------------ 15 | SENDER_AGENT_ID | AgentId | _required_ | An agent identifier sending the event. 16 | RECIPIENT_VERSION | String | _required_ | A version of an application receiving the event. 17 | RECIPIENT_ACCOUNT_ID | AccountId | _required_ | An account identifier of the application receiving the event. 18 | 19 | **Properties** 20 | 21 | Name | Type | Default | Description 22 | ------------------ | ------- | ---------- | ------------------ 23 | type | String | _required_ | Always `event`. 24 | label | String | _required_ | A label describing the event. 25 | -------------------------------------------------------------------------------- /chart/templates/network/external-svc.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.externalService }} 2 | 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: {{ include "mqtt-gateway.fullname" . }}-external 7 | labels: 8 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 9 | annotations: 10 | {{- with .Values.externalService.annotations }} 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | type: LoadBalancer 15 | externalTrafficPolicy: Cluster 16 | sessionAffinity: None 17 | {{- if .Values.externalService.ip }} 18 | loadBalancerIP: {{ .Values.externalService.ip }} 19 | {{- end }} 20 | ports: 21 | {{- if .Values.externalService.ports.mqtt }} 22 | - name: mqtt 23 | port: {{ .Values.externalService.ports.mqtt }} 24 | targetPort: 1883 25 | protocol: TCP 26 | {{- end }} 27 | {{- if .Values.externalService.ports.ws }} 28 | - name: ws 29 | port: {{ .Values.externalService.ports.ws }} 30 | targetPort: 8080 31 | protocol: TCP 32 | {{- end }} 33 | selector: 34 | {{- include "mqtt-gateway.selectorLabels" . | nindent 4 }} 35 | 36 | {{- end }} 37 | -------------------------------------------------------------------------------- /docs/src/messaging-patterns.unicast.response.md: -------------------------------------------------------------------------------- 1 | ## Unicast response 2 | 3 | A response sent to the particular agent. 4 | 5 | **Topic** 6 | 7 | ```bash 8 | agents/${RECIPIENT_AGENT_ID}/api/${RECIPIENT_VERSION}/in/${SENDER_ACCOUNT_ID} 9 | ``` 10 | 11 | **Topic parameters** 12 | 13 | Name | Type | Default | Description 14 | ------------------ | --------- | ---------- | ------------------ 15 | RECIPIENT_AGENT_ID | AgentId | _required_ | An agent identifier receiving the response. 16 | RECIPIENT_VERSION | String | _required_ | A version of an application receiving the response. 17 | SENDER_ACCOUNT_ID | AccountId | _required_ | An account identifier of the application sending the response. 18 | 19 | **Properties** 20 | 21 | Name | Type | Default | Description 22 | ------------------ | ------- | ---------- | ------------------ 23 | type | String | _required_ | Always `response`. 24 | status | String | _required_ | A status code of the response. The value is HTTP compatible. 25 | correlation_data | String | _required_ | The value from the same property of the initial request. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018-2019 Andrei Nesterov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/mqttgw_state.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw_state). 2 | 3 | %% API 4 | -export([ 5 | new/0, 6 | get/1, 7 | find/1, 8 | put/2 9 | ]). 10 | 11 | %% Types 12 | -record(state, { 13 | key, 14 | value 15 | }). 16 | 17 | %% Definitions 18 | -define(STATE_TABLE, ?MODULE). 19 | 20 | %% ============================================================================= 21 | %% API 22 | %% ============================================================================= 23 | 24 | -spec new() -> ok. 25 | new() -> 26 | catch _ = ets:new( 27 | ?STATE_TABLE, 28 | [ set, public, named_table, 29 | {keypos, #state.key}, 30 | {read_concurrency, true} ]), 31 | ok. 32 | 33 | -spec get(any()) -> any(). 34 | get(Key) -> 35 | case find(Key) of 36 | {ok, Val} -> Val; 37 | _ -> error({missing_state_key, Key}) 38 | end. 39 | 40 | -spec find(any()) -> {ok, any()} | error. 41 | find(Key) -> 42 | case ets:lookup(?STATE_TABLE, Key) of 43 | [#state{value = Val}] -> {ok, Val}; 44 | _ -> error 45 | end. 46 | 47 | -spec put(any(), any()) -> ok. 48 | put(Key, Val) -> 49 | ets:insert(?STATE_TABLE, #state{key=Key, value=Val}), 50 | ok. 51 | -------------------------------------------------------------------------------- /chart/templates/network/internal-svc.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.internalService }} 2 | 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: {{ include "mqtt-gateway.fullname" . }}-internal 7 | labels: 8 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 9 | annotations: 10 | {{- with .Values.internalService.annotations }} 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | type: LoadBalancer 15 | externalTrafficPolicy: Cluster 16 | sessionAffinity: None 17 | ports: 18 | {{- if .Values.internalService.ports.mqtt }} 19 | - name: mqtt 20 | port: {{ .Values.internalService.ports.mqtt }} 21 | targetPort: 1883 22 | protocol: TCP 23 | {{- end }} 24 | {{- if .Values.internalService.ports.ws }} 25 | - name: ws 26 | port: {{ .Values.internalService.ports.ws }} 27 | targetPort: 8080 28 | protocol: TCP 29 | {{- end }} 30 | {{- if .Values.clusterService.ports.http }} 31 | - name: http 32 | port: {{ .Values.clusterService.ports.http }} 33 | targetPort: 8081 34 | protocol: TCP 35 | {{- end }} 36 | selector: 37 | {{- include "mqtt-gateway.selectorLabels" . | nindent 4 }} 38 | 39 | {{- end }} 40 | -------------------------------------------------------------------------------- /docs/src/messaging-patterns.unicast.request.md: -------------------------------------------------------------------------------- 1 | ## Unicast request 2 | 3 | A request that must be processed by the particular application's agent. 4 | 5 | **Topic** 6 | 7 | ```bash 8 | agents/${RECIPIENT_AGENT_ID}/api/${RECIPIENT_VERSION}/in/${SENDER_ACCOUNT_ID} 9 | ``` 10 | 11 | **Topic parameters** 12 | 13 | Name | Type | Default | Description 14 | ------------------ | --------- | ---------- | ------------------ 15 | RECIPIENT_AGENT_ID | AgentId | _required_ | An agent identifier receiving the request. 16 | RECIPIENT_VERSION | String | _required_ | A version of an application receiving the request. 17 | SENDER_ACCOUNT_ID | AccountId | _required_ | An account identifier of the application sending the request. 18 | 19 | **Properties** 20 | 21 | Name | Type | Default | Description 22 | ---------------- | ------- | ---------- | ------------------ 23 | type | String | _required_ | Always `request`. 24 | method | String | _required_ | A method of the application to call. 25 | response_topic | String | _required_ | Always `agents/${SENDER_AGENT_ID}/api/${RECIPIENT_VERSION}/in/${RECIPIENT_ACCOUNT_ID}`. 26 | correlation_data | String | _required_ | The same value will be in a response to this request. -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: mqtt-gateway 3 | description: mqtt-gateway broker 4 | home: https://github.com/foxford/mqtt-gateway 5 | 6 | # A chart can be either an 'application' or a 'library' chart. 7 | # 8 | # Application charts are a collection of templates that can be packaged into versioned archives 9 | # to be deployed. 10 | # 11 | # Library charts provide useful utilities or functions for the chart developer. They're included as 12 | # a dependency of application charts to inject those utilities and functions into the rendering 13 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 14 | type: application 15 | 16 | # This is the chart version. This version number should be incremented each time you make changes 17 | # to the chart and its templates, including the app version. 18 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 19 | version: 0.3.0 20 | 21 | # This is the version number of the application being deployed. This version number should be 22 | # incremented each time you make changes to the application. Versions are not expected to 23 | # follow Semantic Versioning. They should reflect the version the application is using. 24 | # It is recommended to use it with quotes. 25 | appVersion: "v0.13.15" 26 | -------------------------------------------------------------------------------- /docs/src/messaging-patterns.multicast.request.md: -------------------------------------------------------------------------------- 1 | ## Multicast request 2 | 3 | A request that may be processed by any agent of the particular application. 4 | 5 | **Topic** 6 | 7 | ```bash 8 | agents/${SENDER_AGENT_ID}/api/${RECIPIENT_VERSION}/out/${RECIPIENT_ACCOUNT_ID} 9 | ``` 10 | 11 | **Topic parameters** 12 | 13 | Name | Type | Default | Description 14 | -------------------- | --------- | ---------- | ------------------ 15 | SENDER_AGENT_ID | AgentId | _required_ | An agent identifier sending the request. 16 | RECIPIENT_VERSION | String | _required_ | A version of an application receiving the request. 17 | RECIPIENT_ACCOUNT_ID | AccountId | _required_ | An account identifier of the application receiving the request. 18 | 19 | **Properties** 20 | 21 | Name | Type | Default | Description 22 | ------------------ | ------- | ---------- | ------------------ 23 | type | String | _required_ | Always `request`. 24 | method | String | _required_ | A method of the application to call. 25 | correlation_data | String | _required_ | The same value will be in a response to this request. 26 | response_topic | String | _required_ | Always `agents/${SENDER_AGENT_ID}/api/${RECIPIENT_VERSION}/in/${RECIPIENT_ACCOUNT_ID}`. 27 | -------------------------------------------------------------------------------- /chart/templates/network/ing.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }} 5 | annotations: 6 | kubernetes.io/ingress.class: nginx 7 | nginx.ingress.kubernetes.io/rewrite-target: /$2 8 | spec: 9 | tls: 10 | - hosts: 11 | - {{ .Values.ingress.host | quote }} 12 | secretName: {{ .Values.tls.secretName }} 13 | rules: 14 | - host: {{ .Values.ingress.host | quote }} 15 | http: 16 | paths: 17 | {{- if .Values.clusterService.ports.mqtt }} 18 | - path: {{ include "mqtt-gateway.ingressPath" . }}(/|$)(mqtt) 19 | pathType: Prefix 20 | backend: 21 | service: 22 | name: {{ include "mqtt-gateway.fullname" . }}-cluster 23 | port: 24 | number: {{ .Values.clusterService.ports.ws }} 25 | {{- end }} 26 | {{- if .Values.clusterService.ports.metrics }} 27 | - path: {{ include "mqtt-gateway.ingressPath" . }}(/|$)(status) 28 | pathType: Prefix 29 | backend: 30 | service: 31 | name: {{ include "mqtt-gateway.fullname" . }}-cluster 32 | port: 33 | number: {{ .Values.clusterService.ports.metrics }} 34 | {{- end }} 35 | -------------------------------------------------------------------------------- /docs/src/message-properties.broker.md: -------------------------------------------------------------------------------- 1 | # Message properties set by the broker 2 | 3 | **Authentication** 4 | 5 | Name | Type | Default | Description 6 | ------------------ | ------- | ---------- | ------------------ 7 | agent_id | AgentId | _required_ | An agent identifier of the sender. 8 | 9 | 10 | 11 | **Connection** 12 | 13 | Name | Type | Default | Description 14 | ------------------ | ------- | ---------- | ------------------ 15 | connection_version | String | _required_ | A version the sender used to connect to the broker. 16 | connection_mode | String | _required_ | A mode the sender used to connect to the broker. 17 | broker_agent_id | AgentId | _required_ | An agent identifier of the broker transmitting the message. 18 | 19 | 20 | 21 | **Tracking a message series** 22 | 23 | Name | Type | Default | Description 24 | ---------------------- | ----------- | ---------- | ------------------ 25 | tracking_id | TrackingId | _required_ | A compound identifier of the message series set by the broker. 26 | session_tracking_label | [SessionId] | _required_ | The list of session identifiers of all the agent participating in the message series. 27 | local_tracking_label | String | _optional_ | A label of the message series set by an agent of the initial message. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = mqttgw 2 | PROJECT_DESCRIPTION = Authentication and authorization plugin for VerneMQ 3 | 4 | define PROJECT_ENV 5 | [ 6 | {vmq_plugin_hooks, [ 7 | {mqttgw, auth_on_register, 5, []}, 8 | {mqttgw, auth_on_register_m5, 6, []}, 9 | {mqttgw, auth_on_publish, 6, []}, 10 | {mqttgw, auth_on_publish_m5, 7, []}, 11 | {mqttgw, on_deliver, 6, []}, 12 | {mqttgw, on_deliver_m5, 7, []}, 13 | {mqttgw, auth_on_subscribe, 3, []}, 14 | {mqttgw, auth_on_subscribe_m5, 4, []}, 15 | {mqttgw, on_topic_unsubscribed, 2, []}, 16 | {mqttgw, on_client_offline, 1, []}, 17 | {mqttgw, on_client_gone, 1, []} 18 | ]} 19 | ] 20 | endef 21 | 22 | DEPS = \ 23 | vernemq_dev \ 24 | toml \ 25 | jose \ 26 | uuid \ 27 | cowboy 28 | 29 | dep_vernemq_dev = git https://github.com/erlio/vernemq_dev.git 6d622aa8c901ae7777433aef2bd049e380c474a6 30 | dep_toml = git https://github.com/dozzie/toml.git v0.3.0 31 | dep_jose = git https://github.com/manifest/jose-erlang v0.1.2 32 | dep_uuid = git https://github.com/okeuday/uuid.git v1.7.5 33 | dep_cowboy = git https://github.com/ninenines/cowboy.git 04ca4c5d31a92d4d3de087bbd7d6021dc4a6d409 34 | 35 | DEP_PLUGINS = version.mk 36 | BUILD_DEPS = version.mk 37 | dep_version.mk = git https://github.com/manifest/version.mk.git v0.2.0 38 | 39 | TEST_DEPS = proper 40 | 41 | SHELL_DEPS = tddreloader 42 | SHELL_OPTS = \ 43 | -eval 'application:ensure_all_started($(PROJECT), permanent)' \ 44 | -s tddreloader start \ 45 | -config rel/sys 46 | 47 | include erlang.mk 48 | 49 | .PHONY: elvis 50 | elvis: 51 | ./elvis rock -c elvis.config 52 | -------------------------------------------------------------------------------- /docs/src/message-properties.application.md: -------------------------------------------------------------------------------- 1 | # Message properties set by the application 2 | 3 | **Tracking time** 4 | 5 | Name | Type | Default | Description 6 | ----------------------------------- | ------- | ---------- | ------------------ 7 | broker_initial_processing_timestamp | i64 | _required_ | Unix time in milliseconds the broker received the initial message of the series. 8 | broker_processing_timestamp | i64 | _required_ | Unix time in milliseconds the broker received the message. 9 | broker_timestamp | i64 | _required_ | Unix time in milliseconds the broker transmitted the message. 10 | local_initial_timediff | i64 | _optional_ | Time difference in milliseconds between local time of an agent connected in the`default` mode and the broker. 11 | initial_timestamp | i64 | _optional_ | Unix time in milliseconds the initial message of the series was created. 12 | timestamp | i64 | _optional_ | Unix time in milliseconds the message was created. 13 | authorization_time | i64 | _optional_ | Time spend by an application on authorization related to the message. 14 | cumulative_authorization_time | i64 | _optional_ | Time spend by an application on authorization related to all messages of the series. 15 | processing_time | i64 | _optional_ | Time spend by an application on processing the message. 16 | cumulative_processing_time | i64 | _optional_ | Time spend by an application on processing all messages of the series. 17 | -------------------------------------------------------------------------------- /chart/templates/network/cluster-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "mqtt-gateway.fullname" . }}-cluster 5 | labels: 6 | {{- include "mqtt-gateway.labels" . | nindent 4 }} 7 | annotations: 8 | {{- with .Values.clusterService.annotations }} 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | spec: 12 | type: ClusterIP 13 | ports: 14 | {{- if .Values.clusterService.ports.mqtts }} 15 | - name: mqtts 16 | port: {{ .Values.clusterService.ports.mqtts }} 17 | targetPort: 8883 18 | protocol: TCP 19 | {{- end }} 20 | {{- if .Values.clusterService.ports.mqtt }} 21 | - name: mqtt 22 | port: {{ .Values.clusterService.ports.mqtt }} 23 | targetPort: 1883 24 | protocol: TCP 25 | {{- end }} 26 | {{- if .Values.clusterService.ports.metrics }} 27 | - name: metrics 28 | port: {{ .Values.clusterService.ports.metrics }} 29 | targetPort: 8888 30 | protocol: TCP 31 | {{- end }} 32 | {{- if .Values.clusterService.ports.wss }} 33 | - name: wss 34 | port: {{ .Values.clusterService.ports.wss }} 35 | targetPort: 8443 36 | protocol: TCP 37 | {{- end }} 38 | {{- if .Values.clusterService.ports.ws }} 39 | - name: ws 40 | port: {{ .Values.clusterService.ports.ws }} 41 | targetPort: 8080 42 | protocol: TCP 43 | {{- end }} 44 | {{- if .Values.clusterService.ports.http }} 45 | - name: http 46 | port: {{ .Values.clusterService.ports.http }} 47 | targetPort: 8081 48 | protocol: TCP 49 | {{- end }} 50 | selector: 51 | {{- include "mqtt-gateway.selectorLabels" . | nindent 4 }} 52 | -------------------------------------------------------------------------------- /chart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for dispatcher. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | app: 8 | image: 9 | repository: cr.yandex/crp1of6bddata8ain3q5/mqtt-gateway 10 | tag: "" 11 | 12 | resources: 13 | limits: 14 | cpu: 10 15 | memory: 10Gi 16 | requests: 17 | cpu: 0.2 18 | memory: 400Mi 19 | 20 | svc: 21 | audience: foobar 22 | credentials: 23 | # foobar-secret-name: 24 | # - subPath: private-key 25 | # mountPath: /path/to/foobar/private/key 26 | # - subPath: public-key 27 | # mountPath: /path/to/foobar/public/key 28 | authz: 29 | # type: local 30 | # trusted: 31 | # - some-service 32 | authn: 33 | # key: /path/to/foobar/public/key 34 | 35 | audiences: 36 | # - audience: foobar 37 | # credentials: 38 | # foobar-pem-secret-name: 39 | # - subPath: foobar-public-key 40 | # mountPath: /path/to/foobar/public/key 41 | # authn: 42 | # key: /path/to/foobar/public/key 43 | 44 | env: 45 | APP_CONFIG: /app/App.toml 46 | APP_AUTHN_ENABLED: 1 47 | APP_AUTHZ_ENABLED: 1 48 | APP_DYNSUB_ENABLED: 1 49 | APP_STAT_ENABLED: 1 50 | APP_RATE_LIMIT_ENABLED: 1 51 | DOCKER_VERNEMQ_DISCOVERY_KUBERNETES: 0 52 | DOCKER_VERNEMQ_KUBERNETES_LABEL_SELECTOR: app.kubernetes.io/name=mqtt-gateway 53 | 54 | clusterService: 55 | ports: 56 | mqtts: 58883 57 | mqtt: 51883 58 | metrics: 58888 59 | wss: 443 60 | ws: 80 61 | http: 8081 62 | 63 | ingress: 64 | host: example.org 65 | 66 | tls: 67 | secretName: tls-certificates 68 | 69 | serviceMonitor: 70 | labels: 71 | release: kube-prometheus-stack 72 | -------------------------------------------------------------------------------- /docker/vernemq.conf: -------------------------------------------------------------------------------- 1 | allow_anonymous = off 2 | allow_register_during_netsplit = off 3 | allow_publish_during_netsplit = off 4 | allow_subscribe_during_netsplit = off 5 | allow_unsubscribe_during_netsplit = off 6 | allow_multiple_sessions = off 7 | max_client_id_size = 150 8 | persistent_client_expiration = never 9 | retry_interval = 5 10 | max_inflight_messages = 0 11 | max_online_messages = -1 12 | max_offline_messages = -1 13 | max_message_size = 0 14 | upgrade_outgoing_qos = off 15 | metadata_plugin = vmq_plumtree 16 | leveldb.maximum_memory.percent = 70 17 | listener.tcp.buffer_sizes = 4096,16384,32768 18 | listener.tcp.my_publisher_listener.buffer_sizes=4096,16384,32768 19 | listener.tcp.my_subscriber_listener.buffer_sizes=4096,16384,32768 20 | listener.max_connections = 10000 21 | listener.nr_of_acceptors = 100 22 | listener.tcp.default = 0.0.0.0:1883 23 | listener.ws.default = 0.0.0.0:8080 24 | listener.vmq.clustering = 0.0.0.0:44053 25 | listener.http.default = 0.0.0.0:8888 26 | listener.tcp.allowed_protocol_versions = 3,4,5 27 | listener.ws.allowed_protocol_versions = 3,4,5 28 | listener.mountpoint = off 29 | systree_enabled = off 30 | systree_interval = 0 31 | shared_subscription_policy = prefer_local 32 | plugins.mqttgw = on 33 | plugins.mqttgw.path = /app/mqttgw 34 | plugins.vmq_passwd = off 35 | plugins.vmq_acl = off 36 | plugins.vmq_diversity = off 37 | plugins.vmq_webhooks = off 38 | plugins.vmq_bridge = off 39 | log.console = console 40 | log.console.level = info 41 | log.syslog = off 42 | log.crash = on 43 | log.crash.file = /vernemq/log/crash.log 44 | log.crash.maximum_message_size = 64KB 45 | log.crash.size = 10MB 46 | log.crash.rotation = $D0 47 | log.crash.rotation.keep = 5 48 | nodename = VerneMQ@127.0.0.1 49 | erlang.async_threads = 64 50 | erlang.max_ports = 262144 51 | erlang.distribution_buffer_size = 32MB 52 | distributed_cookie = vmq 53 | -------------------------------------------------------------------------------- /src/mqttgw_ratelimit.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw_ratelimit). 2 | 3 | %% API 4 | -export([ 5 | read_config/0, 6 | read_config_file/1 7 | ]). 8 | 9 | %% Types 10 | -type config() :: mqttgw_ratelimitstate:constraints(). 11 | 12 | %% ============================================================================= 13 | %% API 14 | %% ============================================================================= 15 | 16 | -spec read_config() -> disabled | {enabled, config()}. 17 | read_config() -> 18 | case os:getenv("APP_RATE_LIMIT_ENABLED", "1") of 19 | "0" -> 20 | error_logger:info_msg("[CONFIG] Rate limits are disabled~n"), 21 | disabled; 22 | _ -> 23 | Config = read_config_file(mqttgw_config:read_config_file()), 24 | error_logger:info_msg("[CONFIG] Rate limits are loaded: ~p~n", [Config]), 25 | {enabled, Config} 26 | end. 27 | 28 | -spec read_config_file(toml:config()) -> config(). 29 | read_config_file(TomlConfig) -> 30 | #{message_count => parse_message_count(TomlConfig), 31 | byte_count => parse_byte_count(TomlConfig)}. 32 | 33 | %% ============================================================================= 34 | %% Internal functions 35 | %% ============================================================================= 36 | 37 | -spec parse_message_count(toml:config()) -> non_neg_integer(). 38 | parse_message_count(TomlConfig) -> 39 | case toml:get_value(["rate_limit"], "message_count", TomlConfig) of 40 | {integer, Val} -> Val; 41 | Other -> error({bad_message_count, Other}) 42 | end. 43 | 44 | -spec parse_byte_count(toml:config()) -> non_neg_integer(). 45 | parse_byte_count(TomlConfig) -> 46 | case toml:get_value(["rate_limit"], "byte_count", TomlConfig) of 47 | {integer, Val} -> Val; 48 | Other -> error({bad_byte_count, Other}) 49 | end. 50 | -------------------------------------------------------------------------------- /deploy.init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ ! ${GITHUB_TOKEN} ]]; then echo "GITHUB_TOKEN is required" 1>&2; exit 1; fi 4 | 5 | PROJECT="${PROJECT:-mqtt-gateway}" 6 | SOURCE=${SOURCE:-"https://api.github.com/repos/netology-group/ulms-env/contents/k8s"} 7 | APPS_SOURCE="https://api.github.com/repos/foxford/ulms-env/contents/apps" 8 | BRANCH="${BRANCH:-master}" 9 | 10 | function FILE_FROM_GITHUB() { 11 | local DEST_DIR="${1}"; if [[ ! "${DEST_DIR}" ]]; then echo "${FUNCNAME[0]}:DEST_DIR is required" 1>&2; exit 1; fi 12 | local URI="${2}"; if [[ ! "${URI}" ]]; then echo "${FUNCNAME[0]}:URI is required" 1>&2; exit 1; fi 13 | if [[ "${3}" != "optional" ]]; then 14 | local FLAGS="-fsSL" 15 | else 16 | local FLAGS="-sSL" 17 | fi 18 | 19 | mkdir -p "${DEST_DIR}" 20 | curl ${FLAGS} \ 21 | -H "authorization: token ${GITHUB_TOKEN}" \ 22 | -H 'accept: application/vnd.github.v3.raw' \ 23 | -o "${DEST_DIR}/$(basename $URI)" \ 24 | "${URI}?ref=${BRANCH}" 25 | } 26 | 27 | function ADD_PROJECT() { 28 | local _PATH="${1}"; if [[ ! "${_PATH}" ]]; then echo "${FUNCNAME[0]}:_PATH is required" 1>&2; exit 1; fi 29 | local _PROJECT="${2}"; if [[ ! "${_PROJECT}" ]]; then echo "${FUNCNAME[0]}:PROJECT is required" 1>&2; exit 1; fi 30 | 31 | tee "${_PATH}" < disabled | enabled. 21 | read_config() -> 22 | case os:getenv("APP_DYNSUB_ENABLED", "1") of 23 | "0" -> 24 | error_logger:info_msg("[CONFIG] Dynamic subscriptions are disabled~n"), 25 | disabled; 26 | _ -> 27 | enabled 28 | end. 29 | 30 | -spec list(subject()) -> [data()]. 31 | list(Subject) -> 32 | filter(vmq_subscriber_db:read({"", Subject})). 33 | 34 | %% ============================================================================= 35 | %% Internal functions 36 | %% ============================================================================= 37 | 38 | filter(undefined) -> 39 | []; 40 | filter(L) -> 41 | lists:flatmap(fun({_, _, S}) -> 42 | lists:foldl( 43 | fun 44 | ({[<<"apps">>, App, <<"api">>, Ver | Object] = Topic, _Qos}, Acc) -> 45 | Data = 46 | #{app => App, 47 | object => Object, 48 | version => Ver, 49 | topic => Topic}, 50 | [Data | Acc]; 51 | ({[<<"broadcasts">>, App, <<"api">>, Ver | Object] = Topic, _Qos}, Acc) -> 52 | Data = 53 | #{app => App, 54 | object => Object, 55 | version => Ver, 56 | topic => Topic}, 57 | [Data | Acc]; 58 | (_, Acc) -> 59 | Acc 60 | end, 61 | [], S) 62 | end, 63 | L). 64 | -------------------------------------------------------------------------------- /src/mqttgw_broker.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw_broker). 2 | 3 | %% API 4 | -export([ 5 | list_connections/0, 6 | subscribe/2, 7 | unsubscribe/2, 8 | publish/3 9 | ]). 10 | 11 | %% ============================================================================= 12 | %% API 13 | %% ============================================================================= 14 | 15 | -spec list_connections() -> [mqttgw:connection()]. 16 | list_connections() -> 17 | vmq_subscriber_db:fold( 18 | fun 19 | ({{_, Subject}, _}, Acc) -> 20 | [Subject|Acc]; 21 | (Item, Acc) -> 22 | error_logger:error_msg( 23 | "Error retrieving a list of the broker connections: " 24 | "invalid format of the list item = '~p', " 25 | "the item is ignored", 26 | [Item]), 27 | Acc 28 | end, 29 | []). 30 | 31 | %% Create a subscription for the particular client. 32 | %% We can verify the fact that subscription has been created by calling: 33 | %% vmq_subscriber_db:read({"", ClientId}). 34 | 35 | subscribe(ClientId, Topics) -> 36 | wait_til_ready(), 37 | CAPSubscribe = vmq_config:get_env(allow_subscribe_during_netsplit, false), 38 | vmq_reg:subscribe(CAPSubscribe, {"", ClientId}, Topics), 39 | ok. 40 | 41 | unsubscribe(ClientId, Topics) -> 42 | wait_til_ready(), 43 | CAPSubscribe = vmq_config:get_env(allow_unsubscribe_during_netsplit, false), 44 | vmq_reg:unsubscribe(CAPSubscribe, {"", ClientId}, Topics), 45 | ok. 46 | 47 | publish(Topic, Payload, QoS) -> 48 | {_, Publish, _} = vmq_reg:direct_plugin_exports(mqttgw), 49 | Publish(Topic, Payload, #{qos => QoS}), 50 | ok. 51 | 52 | %% ============================================================================= 53 | %% Internal functions 54 | %% ============================================================================= 55 | 56 | -spec wait_til_ready() -> ok. 57 | wait_til_ready() -> 58 | case catch vmq_cluster:if_ready(fun() -> true end, []) of 59 | true -> 60 | ok; 61 | _ -> 62 | timer:sleep(100), 63 | wait_til_ready() 64 | end. 65 | -------------------------------------------------------------------------------- /src/mqttgw_ratelimitstate.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw_ratelimitstate). 2 | 3 | %% API 4 | -export([ 5 | new/0, 6 | get_dirty/1, 7 | get/2, 8 | put_dirty/2, 9 | put/4, 10 | erase/0 11 | ]). 12 | 13 | %% Types 14 | -type constraints() :: 15 | #{message_count := non_neg_integer(), 16 | byte_count := non_neg_integer()}. 17 | -type data() :: 18 | #{time := non_neg_integer(), 19 | bytes := non_neg_integer()}. 20 | -type error_reason() :: {atom(), non_neg_integer()}. 21 | -type result() :: 22 | #{messages := [data()], 23 | message_count := non_neg_integer(), 24 | byte_count := non_neg_integer()}. 25 | 26 | -record(item, { 27 | key :: mqttgw_id:agent_id(), 28 | val :: [data()] 29 | }). 30 | 31 | -export_types([data/0, result/0]). 32 | 33 | %% Definitions 34 | -define(AGENT_TABLE, ?MODULE). 35 | 36 | %% ============================================================================= 37 | %% API 38 | %% ============================================================================= 39 | 40 | -spec new() -> ok. 41 | new() -> 42 | catch _ = ets:new( 43 | ?AGENT_TABLE, 44 | [ set, public, named_table, 45 | {keypos, #item.key} ]), 46 | ok. 47 | 48 | -spec get_dirty(mqttgw_id:agent_id()) -> [data()]. 49 | get_dirty(AgentId) -> 50 | case ets:lookup(?AGENT_TABLE, AgentId) of 51 | [#item{val=L}] -> L; 52 | _ -> [] 53 | end. 54 | 55 | -spec get(mqttgw_id:agent_id(), non_neg_integer()) -> result(). 56 | get(AgentId, Time) -> 57 | L = lists:filter(fun(#{time := T}) -> T > Time end, get_dirty(AgentId)), 58 | BytesCount = lists:foldl(fun(#{bytes := B}, Acc) -> Acc + B end, 0, L), 59 | MessageCount = length(L), 60 | #{messages => L, 61 | message_count => MessageCount, 62 | byte_count => BytesCount}. 63 | 64 | -spec put_dirty(mqttgw_id:agent_id(), data()) -> ok. 65 | put_dirty(AgentId, Data) -> 66 | L = get_dirty(AgentId), 67 | Item = #item{key=AgentId, val=[Data|L]}, 68 | ets:insert(?AGENT_TABLE, Item), 69 | ok. 70 | 71 | -spec put(mqttgw_id:agent_id(), data(), non_neg_integer(), constraints()) 72 | -> ok | {error, error_reason()}. 73 | put(AgentId, Data, Time, Constraints) -> 74 | #{bytes := Bytes} = Data, 75 | #{message_count := MessageCountLimit, 76 | byte_count := ByteCountLimit} = Constraints, 77 | case get(AgentId, Time) of 78 | #{message_count := MessageCount} when (MessageCount + 1) > MessageCountLimit -> 79 | {error, {message_rate_exceeded, MessageCount + 1}}; 80 | #{byte_count := ByteCount} when (ByteCount + Bytes) > ByteCountLimit -> 81 | {error, {byte_rate_exceeded, ByteCount + Bytes}}; 82 | #{messages := L} -> 83 | Item = #item{key=AgentId, val=[Data|L]}, 84 | ets:insert(?AGENT_TABLE, Item), 85 | ok 86 | end. 87 | 88 | -spec erase() -> ok. 89 | erase() -> 90 | ets:delete_all_objects(?AGENT_TABLE), 91 | ok. 92 | -------------------------------------------------------------------------------- /src/mqttgw_authz.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw_authz). 2 | 3 | %% API 4 | -export([ 5 | read_config/0, 6 | read_config_file/1, 7 | authorize/3 8 | ]). 9 | 10 | %% Types 11 | -type config() :: map(). 12 | 13 | -export_types([config/0]). 14 | 15 | %% ============================================================================= 16 | %% API 17 | %% ============================================================================= 18 | 19 | -spec read_config() -> disabled | {enabled, config()}. 20 | read_config() -> 21 | case os:getenv("APP_AUTHZ_ENABLED", "1") of 22 | "0" -> 23 | error_logger:info_msg("[CONFIG] Authz is disabled~n"), 24 | disabled; 25 | _ -> 26 | TomlConfig = mqttgw_config:read_config_file(), 27 | Config = read_config_file(TomlConfig), 28 | error_logger:info_msg("[CONFIG] Authz is loaded: ~p~n", [Config]), 29 | {enabled, Config} 30 | end. 31 | 32 | -spec read_config_file(toml:config()) -> config(). 33 | read_config_file(TomlConfig) -> 34 | toml:folds( 35 | ["authz"], 36 | fun(_Config, Section, Acc) -> 37 | Aud = parse_audience(Section), 38 | Type = parse_type(Section, TomlConfig), 39 | Trusted = parse_trusted(Section, TomlConfig), 40 | Acc#{Aud => #{type => Type, trusted => Trusted}} 41 | end, 42 | #{}, 43 | TomlConfig). 44 | 45 | -spec authorize(binary(), mqttgw_authn:account_id(), config()) -> ok. 46 | authorize(Audience, AccountId, Config) -> 47 | case maps:find(Audience, Config) of 48 | {ok, Inner} -> 49 | #{trusted := Trusted} = Inner, 50 | case gb_sets:is_member(AccountId, Trusted) of 51 | true -> ok; 52 | _ -> error({nomatch_trusted, AccountId, Trusted}) 53 | end; 54 | _ -> 55 | error({missing_authz_config, Audience}) 56 | end. 57 | 58 | %% ============================================================================= 59 | %% Internal functions 60 | %% ============================================================================= 61 | 62 | -spec parse_audience(toml:section()) -> binary(). 63 | parse_audience(["authz", Aud]) when is_list(Aud) -> 64 | list_to_binary(Aud); 65 | parse_audience(Val) -> 66 | error({bad_audience, Val}). 67 | 68 | -spec parse_type(toml:section(), toml:config()) -> local. 69 | parse_type(Section, Config) -> 70 | case toml:get_value(Section, "type", Config) of 71 | {string, "local"} -> local; 72 | none -> error(missing_type); 73 | Other -> error({bad_type, Other}) 74 | end. 75 | 76 | -spec parse_trusted(toml:section(), toml:config()) -> gb_sets:set(). 77 | parse_trusted(Section, Config) -> 78 | case toml:get_value(Section, "trusted", Config) of 79 | {array, {string, L}} -> 80 | gb_sets:from_list(lists:map(fun(Val) -> 81 | mqttgw_authn:parse_account_id(list_to_binary(Val)) 82 | end, L)); 83 | none -> 84 | error(missing_trusted); 85 | Other -> 86 | error({bad_trusted, Other}) 87 | end. 88 | -------------------------------------------------------------------------------- /chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "mqtt-gateway.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "mqtt-gateway.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "mqtt-gateway.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "mqtt-gateway.labels" -}} 37 | helm.sh/chart: {{ include "mqtt-gateway.chart" . }} 38 | {{ include "mqtt-gateway.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "mqtt-gateway.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "mqtt-gateway.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Short namespace. 55 | */}} 56 | {{- define "mqtt-gateway.shortNamespace" -}} 57 | {{- $shortns := regexSplit "-" .Release.Namespace -1 | first }} 58 | {{- if has $shortns (list "production" "p") }} 59 | {{- else }} 60 | {{- $shortns }} 61 | {{- end }} 62 | {{- end }} 63 | 64 | {{/* 65 | Namespace in ingress path. 66 | converts as follows: 67 | - testing01 -> t01 68 | - staging01-classroom-ng -> s01/classroom-foxford 69 | - production-webinar-ng -> webinar-foxford 70 | */}} 71 | {{- define "mqtt-gateway.ingressPathNamespace" -}} 72 | {{- $ns_head := regexSplit "-" .Release.Namespace -1 | first }} 73 | {{- $ns_tail := regexSplit "-" .Release.Namespace -1 | rest | join "-" | replace "ng" "foxford" }} 74 | {{- if has $ns_head (list "production" "p") }} 75 | {{- $ns_tail }} 76 | {{- else }} 77 | {{- list (regexReplaceAll "(.)[^\\d]*(.+)" $ns_head "${1}${2}") $ns_tail | compact | join "/" }} 78 | {{- end }} 79 | {{- end }} 80 | 81 | {{/* 82 | Ingress path. 83 | */}} 84 | {{- define "mqtt-gateway.ingressPath" -}} 85 | {{- list "" (include "mqtt-gateway.ingressPathNamespace" .) (include "mqtt-gateway.fullname" .) | join "/" }} 86 | {{- end }} 87 | 88 | {{/* 89 | Create volumeMount name from audience and secret name 90 | */}} 91 | {{- define "mqtt-gateway.volumeMountName" -}} 92 | {{- $audience := index . 0 -}} 93 | {{- $secret := index . 1 -}} 94 | {{- printf "%s-%s-secret" $audience $secret | replace "." "-" | trunc 63 | trimSuffix "-" }} 95 | {{- end }} 96 | -------------------------------------------------------------------------------- /src/mqttgw_id.erl: -------------------------------------------------------------------------------- 1 | -module(mqttgw_id). 2 | 3 | %% API 4 | -export([ 5 | read_config/0, 6 | read_config_file/1, 7 | account_id/1, 8 | account_label/1, 9 | audience/1, 10 | label/1, 11 | format_account_id/1, 12 | format_agent_id/1 13 | ]). 14 | 15 | %% Types 16 | -type agent_id() :: #{label := binary(), account_id := mqttgw_authn:account_id()}. 17 | 18 | -export_types([agent_id/0]). 19 | 20 | %% ============================================================================= 21 | %% API 22 | %% ============================================================================= 23 | 24 | -spec read_config() -> agent_id(). 25 | read_config() -> 26 | handle_config_file(error). 27 | 28 | -spec read_config_file(toml:config()) -> agent_id(). 29 | read_config_file(TomlConfig) -> 30 | handle_config_file({ok, TomlConfig}). 31 | 32 | -spec account_id(agent_id()) -> mqttgw_authn:account_id(). 33 | account_id(AgentId) -> 34 | maps:get(account_id, AgentId). 35 | 36 | -spec account_label(agent_id()) -> binary(). 37 | account_label(AgentId) -> 38 | maps:get(label, maps:get(account_id, AgentId)). 39 | 40 | -spec audience(agent_id()) -> binary(). 41 | audience(AgentId) -> 42 | maps:get(audience, maps:get(account_id, AgentId)). 43 | 44 | -spec label(agent_id()) -> binary(). 45 | label(AgentId) -> 46 | maps:get(label, AgentId). 47 | 48 | -spec format_account_id(agent_id()) -> binary(). 49 | format_account_id(AgentId) -> 50 | #{account_id := AccountId} = AgentId, 51 | mqttgw_authn:format_account_id(AccountId). 52 | 53 | -spec format_agent_id(agent_id()) -> binary(). 54 | format_agent_id(AgentId) -> 55 | #{label := Label, 56 | account_id := AccountId} = AgentId, 57 | <