From 41452a182d5b2c8e0ff25ae1b73d457d5fd80fb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 06:43:45 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20revamp=20repo=20for=20open-source?= =?UTF-8?q?=20=E2=80=93=20remove=20Suez=20refs,=20add=20server/client/helm?= =?UTF-8?q?/ci/docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/perspikapps/edgekit/sessions/6abf6214-6f32-4a54-be1c-8704e4c63d7c Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com> --- .github/workflows/ci.yml | 75 +++ .github/workflows/release.yml | 121 ++++ .gitignore | 23 + LICENSE | 173 ++++++ README.md | 195 +++++- client/Dockerfile | 21 + client/package-lock.json | 567 ++++++++++++++++++ client/package.json | 14 + client/src/index.js | 108 ++++ docker-compose.yml | 43 ++ docs/architecture.md | 122 ++++ docs/quickstart.md | 167 ++++++ helm/edgekit/Chart.yaml | 19 + helm/edgekit/templates/NOTES.txt | 25 + helm/edgekit/templates/_helpers.tpl | 63 ++ helm/edgekit/templates/client-deployment.yaml | 54 ++ helm/edgekit/templates/server-deployment.yaml | 71 +++ helm/edgekit/templates/server-pvc.yaml | 18 + helm/edgekit/templates/server-service.yaml | 23 + helm/edgekit/values.yaml | 79 +++ scripts/build.sh | 25 + scripts/start-local.sh | 19 + scripts/stop-local.sh | 12 + server/Dockerfile | 15 + server/entrypoint.sh | 8 + server/mosquitto.conf | 19 + 26 files changed, 2078 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 client/Dockerfile create mode 100644 client/package-lock.json create mode 100644 client/package.json create mode 100644 client/src/index.js create mode 100644 docker-compose.yml create mode 100644 docs/architecture.md create mode 100644 docs/quickstart.md create mode 100644 helm/edgekit/Chart.yaml create mode 100644 helm/edgekit/templates/NOTES.txt create mode 100644 helm/edgekit/templates/_helpers.tpl create mode 100644 helm/edgekit/templates/client-deployment.yaml create mode 100644 helm/edgekit/templates/server-deployment.yaml create mode 100644 helm/edgekit/templates/server-pvc.yaml create mode 100644 helm/edgekit/templates/server-service.yaml create mode 100644 helm/edgekit/values.yaml create mode 100755 scripts/build.sh create mode 100755 scripts/start-local.sh create mode 100755 scripts/stop-local.sh create mode 100644 server/Dockerfile create mode 100644 server/entrypoint.sh create mode 100644 server/mosquitto.conf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..587588b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,75 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + lint-helm: + name: Lint Helm chart + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: "3.14.0" + + - name: Helm lint + run: helm lint helm/edgekit + + build-server: + name: Build server image + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build server image + uses: docker/build-push-action@v6 + with: + context: ./server + push: false + tags: edgekit-server:ci + cache-from: type=gha + cache-to: type=gha,mode=max + + build-client: + name: Build client image + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: client/package-lock.json + + - name: Install dependencies + working-directory: client + run: npm ci + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build client image + uses: docker/build-push-action@v6 + with: + context: ./client + push: false + tags: edgekit-client:ci + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6ce356d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,121 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +env: + REGISTRY: ghcr.io + IMAGE_ORG: perspikapps + +jobs: + release-server: + name: Release server image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta (server) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/edgekit-server + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push server image + uses: docker/build-push-action@v6 + with: + context: ./server + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + release-client: + name: Release client image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta (client) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/edgekit-client + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push client image + uses: docker/build-push-action@v6 + with: + context: ./client + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + release-helm: + name: Package and release Helm chart + runs-on: ubuntu-latest + needs: [release-server, release-client] + permissions: + contents: write + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: "3.14.0" + + - name: Log in to GHCR (OCI registry for Helm) + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ${{ env.REGISTRY }} \ + --username ${{ github.actor }} \ + --password-stdin + + - name: Package Helm chart + run: helm package helm/edgekit --destination /tmp/helm-output + + - name: Push Helm chart to GHCR + run: | + helm push /tmp/helm-output/edgekit-*.tgz \ + oci://${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/charts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8626afa --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Node.js +node_modules/ +npm-debug.log* + +# Build outputs +dist/ +build/ + +# Docker +.docker/ + +# Helm +*.tgz + +# OS +.DS_Store +Thumbs.db + +# Editor +.vscode/ +.idea/ +*.swp +*.swo diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a306d83 --- /dev/null +++ b/LICENSE @@ -0,0 +1,173 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship made available under + the License, as indicated by a copyright notice that is included in + or attached to the work (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other transformations + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean, as submitted to the Licensor for inclusion + in the Work by the copyright owner or by an individual or Legal Entity + authorized to submit on behalf of the copyright owner. For the purposes + of this definition, "submitted" means any form of electronic, verbal, + or written communication sent to the Licensor or its representatives, + including but not limited to communication on electronic mailing lists, + source code control systems, and issue tracking systems that are managed + by, or on behalf of, the Licensor for the purpose of discussing and + improving the Work, but excluding communication that is conspicuously + marked or designated in writing by the copyright owner as "Not a + Contribution." + + "Contributor" shall mean Licensor and any Legal Entity on behalf of + whom a Contribution has been received by the Licensor and included + within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent contributions + Licensable by such Contributor that are necessarily infringed by their + Contribution(s) alone or by the combined work (as submitted) with the + Work to which such Contribution(s) was submitted. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative + Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, You must include a readable copy of the + attribution notices contained within such NOTICE file, in + at least one of the following places: within a NOTICE text + file distributed as part of the Derivative Works; within + the Source form or documentation, if provided along with the + Derivative Works; or, within a display generated by the + Derivative Works, if and wherever such third-party notices + normally appear. The contents of the NOTICE file are for + informational purposes only and do not modify the License. + You may add Your own attribution notices within Derivative + Works that You distribute, alongside or in addition to the + NOTICE text from the Work, provided that such additional + attribution notices cannot be construed as modifying the License. + + You may add Your own license statement for Your modifications and + may provide additional grant of rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the + Contribution, either as expressly stated herein, or with separate + terms and conditions. However, Your use of this software is governed + by this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or reproducing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or exemplary damages of any character arising as a + result of this License or out of the use or inability to use the + Work (even if such Contributor has been advised of the possibility + of such damages). + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may offer only + conditions consistent with this License. + + END OF TERMS AND CONDITIONS + + Copyright 2024 edgekit contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index c30d0dc..bf6ae65 100644 --- a/README.md +++ b/README.md @@ -1 +1,194 @@ -# edgekit \ No newline at end of file +# EdgeKit + +**EdgeKit** is an open-source client/server IoT edge platform designed to run on [k3s](https://k3s.io/). +It provides a central MQTT broker (server) and lightweight edge agents (clients) that collect telemetry and stream it to the server via MQTT over WebSocket. + +--- + +## Architecture + +``` +┌─────────────────────────────────────────┐ +│ k3s / Kubernetes │ +│ │ +│ ┌───────────────────────────────────┐ │ +│ │ edgekit-server │ │ +│ │ Eclipse Mosquitto MQTT broker │ │ +│ │ • port 1883 – plain MQTT │ │ +│ │ • port 9001 – MQTT over WS │ │ +│ └───────────────────────────────────┘ │ +│ ▲ ▲ │ +│ │ WebSocket │ │ +│ ┌─────────┴──┐ ┌──────┴─────────┐ │ +│ │ client 1 │ │ client N │ │ +│ │ edge agent │ │ edge agent │ │ +│ └────────────┘ └────────────────┘ │ +└─────────────────────────────────────────┘ +``` + +Each **client** is a single Docker container that: +- Collects system metrics (CPU, memory, disk, network) +- Publishes JSON payloads to `edgekit//metrics` every 5 seconds (configurable) +- Automatically reconnects to the broker on failure + +The **server** is a single Eclipse Mosquitto container with both plain MQTT (1883) and WebSocket (9001) listeners. + +For a deeper dive, see [docs/architecture.md](docs/architecture.md). + +--- + +## Quick Start (local) + +### Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) ≥ 24 +- [Docker Compose](https://docs.docker.com/compose/) v2 + +### 1. Clone the repository + +```bash +git clone https://github.com/perspikapps/edgekit.git +cd edgekit +``` + +### 2. Start the stack + +```bash +./scripts/start-local.sh +``` + +This builds and starts: +- `edgekit-server` – MQTT broker (ports 1883 and 9001 exposed on localhost) +- `edgekit-client` – Edge agent publishing metrics every 5 seconds + +### 3. Watch metrics flow + +```bash +# Subscribe to all edgekit topics +docker run --rm --network host eclipse-mosquitto:2.0 \ + mosquitto_sub -h localhost -t "edgekit/#" -v +``` + +### 4. Stop the stack + +```bash +./scripts/stop-local.sh +``` + +For a full walkthrough, see [docs/quickstart.md](docs/quickstart.md). + +--- + +## Deploy on k3s + +### Prerequisites + +- A running k3s cluster +- [Helm](https://helm.sh/docs/intro/install/) ≥ 3.14 +- `kubectl` configured to reach the cluster + +### Install with Helm + +```bash +# Install edgekit (server + 1 client) +helm install edgekit oci://ghcr.io/perspikapps/charts/edgekit \ + --namespace edgekit \ + --create-namespace + +# Scale to multiple clients +helm upgrade edgekit oci://ghcr.io/perspikapps/charts/edgekit \ + --namespace edgekit \ + --set client.replicaCount=3 +``` + +Or from the local chart: + +```bash +helm install edgekit ./helm/edgekit \ + --namespace edgekit \ + --create-namespace +``` + +### Verify + +```bash +kubectl -n edgekit get pods +kubectl -n edgekit logs -f -l app.kubernetes.io/component=client +``` + +--- + +## Configuration + +All configuration is via environment variables (client) and `values.yaml` (Helm). + +| Variable | Default | Description | +|---|---|---| +| `MQTT_BROKER_URL` | `ws://edgekit-server:9001` | WebSocket URL of the MQTT broker | +| `MQTT_TOPIC_PREFIX` | `edgekit` | Topic namespace prefix | +| `CLIENT_ID` | auto-generated | Unique identifier for this edge agent | +| `PUBLISH_INTERVAL_MS` | `5000` | Metrics publish interval in milliseconds | + +See [helm/edgekit/values.yaml](helm/edgekit/values.yaml) for the full Helm configuration reference. + +--- + +## Repository Layout + +``` +edgekit/ +├── server/ # MQTT broker container +│ ├── Dockerfile +│ ├── mosquitto.conf +│ └── entrypoint.sh +├── client/ # Edge agent container +│ ├── Dockerfile +│ ├── package.json +│ └── src/ +│ └── index.js +├── helm/ +│ └── edgekit/ # Root Helm chart +│ ├── Chart.yaml +│ ├── values.yaml +│ └── templates/ +├── scripts/ +│ ├── build.sh # Build Docker images +│ ├── start-local.sh # Start via docker compose +│ └── stop-local.sh # Stop local stack +├── .github/ +│ └── workflows/ +│ ├── ci.yml # CI: lint + build on PR +│ └── release.yml # Release: push images + Helm chart on tag +├── docs/ +│ ├── architecture.md +│ └── quickstart.md +└── docker-compose.yml # Local development +``` + +--- + +## CI / CD + +| Workflow | Trigger | What it does | +|---|---|---| +| `ci.yml` | push / PR to `main` | Lints Helm chart, builds both Docker images | +| `release.yml` | push of `v*.*.*` tag | Builds & pushes images to GHCR, packages & pushes Helm chart to GHCR OCI registry | + +To release a new version: + +```bash +git tag v1.0.0 +git push origin v1.0.0 +``` + +--- + +## Contributing + +Pull requests are welcome. Please open an issue first to discuss what you would like to change. + +--- + +## License + +[Apache 2.0](LICENSE) \ No newline at end of file diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..8a25ee0 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,21 @@ +FROM node:20-alpine + +LABEL org.opencontainers.image.title="edgekit-client" +LABEL org.opencontainers.image.description="Edge agent for edgekit – collects metrics and streams to central MQTT broker" +LABEL org.opencontainers.image.source="https://github.com/perspikapps/edgekit" + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev + +COPY src/ ./src/ + +ENV MQTT_BROKER_URL=ws://edgekit-server:9001 +ENV MQTT_TOPIC_PREFIX=edgekit +ENV CLIENT_ID=edge-local-1 +ENV PUBLISH_INTERVAL_MS=5000 + +USER node + +CMD ["node", "src/index.js"] diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..5e98d90 --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,567 @@ +{ + "name": "edgekit-client", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "edgekit-client", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "mqtt": "^5.10.1", + "systeminformation": "^5.31.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/broker-factory": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.14.tgz", + "integrity": "sha512-L45k5HMbPIrMid0nTOZ/UPXG/c0aRuQKVrSDFIb1zOkvfiyHgYmIjc3cSiN1KwQIvRDOtKE0tfb3I9EZ3CmpQQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-factory": "^7.0.49" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-unique-numbers": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz", + "integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mqtt": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.1.tgz", + "integrity": "sha512-V1WnkGuJh3ec9QXzy5Iylw8OOBK+Xu1WhxcQ9mMpLThG+/JZIMV1PgLNRgIiqXhZnvnVLsuyxHl5A/3bHHbcAA==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/systeminformation": { + "version": "5.31.5", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.5.tgz", + "integrity": "sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/worker-factory": { + "version": "7.0.49", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.49.tgz", + "integrity": "sha512-lW7tpgy6aUv2dFsQhv1yv+XFzdkCf/leoKRTGMPVK5/die6RrUjqgJHJf556qO+ZfytNG6wPXc17E8zzsOLUDw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.31", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.31.tgz", + "integrity": "sha512-ngkq5S6JuZyztom8tDgBzorLo9byhBMko/sXfgiUD945AuzKGg1GCgDMCC3NaYkicLpGKXutONM36wEX8UbBCA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.16", + "worker-timers-worker": "^9.0.14" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.16.tgz", + "integrity": "sha512-JyP3AvUGyPGbBGW7XiUewm2+0pN/aYo1QpVf5kdXAfkDZcN3p7NbWrG6XnyDEpDIvfHk/+LCnOW/NsuiU9riYA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "broker-factory": "^3.1.14", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.14" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.14.tgz", + "integrity": "sha512-/qF06C60sXmSLfUl7WglvrDIbspmPOM8UrG63Dnn4bi2x4/DfqHS/+dxF5B+MdHnYO5tVuZYLHdAodrKdabTIg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1", + "worker-factory": "^7.0.49" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..641e8c9 --- /dev/null +++ b/client/package.json @@ -0,0 +1,14 @@ +{ + "name": "edgekit-client", + "version": "1.0.0", + "description": "Edge agent – collects system metrics and streams to central MQTT broker", + "main": "src/index.js", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "dependencies": { + "mqtt": "^5.10.1", + "systeminformation": "^5.31.5" + } +} diff --git a/client/src/index.js b/client/src/index.js new file mode 100644 index 0000000..9424a42 --- /dev/null +++ b/client/src/index.js @@ -0,0 +1,108 @@ +'use strict'; + +const mqtt = require('mqtt'); +const si = require('systeminformation'); + +const BROKER_URL = process.env.MQTT_BROKER_URL || 'ws://edgekit-server:9001'; +const TOPIC_PREFIX = process.env.MQTT_TOPIC_PREFIX || 'edgekit'; +const CLIENT_ID = process.env.CLIENT_ID || `edge-${Math.random().toString(16).slice(2, 8)}`; +const INTERVAL_MS = parseInt(process.env.PUBLISH_INTERVAL_MS || '5000', 10); + +console.log(`[edgekit-client] Starting — id=${CLIENT_ID} broker=${BROKER_URL}`); + +const client = mqtt.connect(BROKER_URL, { + clientId: CLIENT_ID, + reconnectPeriod: 5000, + connectTimeout: 30000, +}); + +client.on('connect', () => { + console.log(`[edgekit-client] Connected to ${BROKER_URL}`); + startPublishing(); +}); + +client.on('error', (err) => { + console.error('[edgekit-client] MQTT error:', err.message); +}); + +client.on('reconnect', () => { + console.log('[edgekit-client] Reconnecting…'); +}); + +client.on('close', () => { + console.log('[edgekit-client] Connection closed'); +}); + +async function collectMetrics() { + const [cpu, mem, fsSize, networkStats, time] = await Promise.all([ + si.currentLoad(), + si.mem(), + si.fsSize(), + si.networkStats(), + si.time(), + ]); + + return { + clientId: CLIENT_ID, + timestamp: new Date().toISOString(), + uptime: time.uptime, + cpu: { + loadPercent: parseFloat(cpu.currentLoad.toFixed(2)), + cores: cpu.cpus ? cpu.cpus.length : undefined, + }, + memory: { + totalBytes: mem.total, + usedBytes: mem.used, + freeBytes: mem.free, + usedPercent: parseFloat(((mem.used / mem.total) * 100).toFixed(2)), + }, + filesystems: (fsSize || []).map((fs) => ({ + mount: fs.mount, + type: fs.type, + totalBytes: fs.size, + usedBytes: fs.used, + usedPercent: parseFloat(fs.use.toFixed(2)), + })), + network: (networkStats || []).slice(0, 4).map((iface) => ({ + iface: iface.iface, + rxBytesTotal: iface.rx_bytes, + txBytesTotal: iface.tx_bytes, + })), + }; +} + +let publishTimer = null; + +async function startPublishing() { + const publish = async () => { + try { + const metrics = await collectMetrics(); + const topic = `${TOPIC_PREFIX}/${CLIENT_ID}/metrics`; + client.publish(topic, JSON.stringify(metrics), { qos: 1 }, (err) => { + if (err) { + console.error('[edgekit-client] Publish error:', err.message); + } else { + console.log(`[edgekit-client] Published to ${topic}`); + } + }); + } catch (err) { + console.error('[edgekit-client] Collect error:', err.message); + } + }; + + // Publish immediately, then on interval + await publish(); + publishTimer = setInterval(publish, INTERVAL_MS); +} + +process.on('SIGTERM', () => { + console.log('[edgekit-client] SIGTERM received, shutting down…'); + if (publishTimer) clearInterval(publishTimer); + client.end(false, {}, () => process.exit(0)); +}); + +process.on('SIGINT', () => { + console.log('[edgekit-client] SIGINT received, shutting down…'); + if (publishTimer) clearInterval(publishTimer); + client.end(false, {}, () => process.exit(0)); +}); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..19e1a62 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: "3.9" + +# EdgeKit – local development stack +# Runs 1 server (MQTT broker) + 1 client (edge agent) +# Usage: docker compose up --build + +services: + server: + build: + context: ./server + dockerfile: Dockerfile + image: edgekit-server:local + container_name: edgekit-server + restart: unless-stopped + ports: + - "1883:1883" # MQTT + - "9001:9001" # MQTT over WebSocket + volumes: + - mosquitto-data:/mosquitto/data + healthcheck: + test: ["CMD", "mosquitto_pub", "-h", "localhost", "-t", "healthcheck", "-m", "ping", "-q", "0"] + interval: 10s + timeout: 5s + retries: 5 + + client: + build: + context: ./client + dockerfile: Dockerfile + image: edgekit-client:local + container_name: edgekit-client + restart: unless-stopped + depends_on: + server: + condition: service_healthy + environment: + MQTT_BROKER_URL: ws://server:9001 + MQTT_TOPIC_PREFIX: edgekit + CLIENT_ID: edge-local-1 + PUBLISH_INTERVAL_MS: "5000" + +volumes: + mosquitto-data: diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..4da2036 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,122 @@ +# Architecture + +## Overview + +EdgeKit is a lightweight IoT edge platform composed of two types of containers: + +| Component | Image | Role | +|---|---|---| +| **server** | `edgekit-server` | Central Eclipse Mosquitto MQTT broker | +| **client** | `edgekit-client` | Edge agent – collects metrics, streams to server | + +--- + +## Server + +The server is a single [Eclipse Mosquitto](https://mosquitto.org/) instance configured with two listeners: + +| Port | Protocol | Usage | +|------|----------|-------| +| 1883 | MQTT (TCP) | Internal cluster communication | +| 9001 | MQTT over WebSocket | Browser clients, external tooling | + +**Configuration file**: `server/mosquitto.conf` + +Mosquitto is the de-facto standard lightweight MQTT broker for IoT workloads. It handles all pub/sub routing; no custom server code is required. + +### Data persistence + +When deployed via Helm, broker state (retained messages, etc.) is stored in a `PersistentVolumeClaim`. In local Docker Compose mode, a named Docker volume is used. + +--- + +## Client + +The client is a Node.js application that: + +1. Connects to the MQTT broker via WebSocket (`MQTT_BROKER_URL`) +2. Periodically collects system metrics using the [`systeminformation`](https://systeminformation.io/) library +3. Serialises metrics as JSON and publishes them to `//metrics` + +### Published payload format + +```json +{ + "clientId": "edge-local-1", + "timestamp": "2024-01-15T10:30:00.000Z", + "uptime": 123456, + "cpu": { + "loadPercent": 12.34, + "cores": 4 + }, + "memory": { + "totalBytes": 8589934592, + "usedBytes": 3221225472, + "freeBytes": 5368709120, + "usedPercent": 37.50 + }, + "filesystems": [ + { + "mount": "/", + "type": "ext4", + "totalBytes": 107374182400, + "usedBytes": 21474836480, + "usedPercent": 20.00 + } + ], + "network": [ + { + "iface": "eth0", + "rxBytesTotal": 104857600, + "txBytesTotal": 52428800 + } + ] +} +``` + +### Reconnect behaviour + +The MQTT client uses exponential-back-off reconnection (built into the `mqtt` npm package) with a 5-second base period. The container exits cleanly on SIGTERM/SIGINT, draining in-flight publishes first. + +--- + +## Topic structure + +``` +edgekit/ +└── / + └── metrics ← JSON telemetry (QoS 1) +``` + +Consumers can subscribe to `edgekit/#` to receive all metrics from all clients, or `edgekit//metrics` for a specific agent. + +--- + +## Kubernetes / k3s deployment + +``` +Namespace: edgekit +│ +├── Deployment: edgekit-server (replicas: 1) +│ └── Container: mosquitto +├── Service: edgekit-server +│ ├── ClusterIP :1883 (mqtt) +│ └── ClusterIP :9001 (websockets) +├── PersistentVolumeClaim: edgekit-server-data +│ +└── Deployment: edgekit-client (replicas: N) + └── Container: edge-agent +``` + +The Helm chart at `helm/edgekit/` provisions all of the above resources. The client pods use the Kubernetes downward API to set `CLIENT_ID` from `metadata.name`, ensuring each replica has a unique identifier. + +--- + +## Extending EdgeKit + +The architecture is intentionally minimal. Common extensions: + +- **Add authentication**: Configure Mosquitto password files or TLS certificates via a Kubernetes Secret mounted into the server container. +- **Add a dashboard**: Deploy [MQTT Explorer](https://mqtt-explorer.com/) or [Grafana + EMQX](https://docs.emqx.com/en/emqx/latest/dashboard/introduction.html) as additional pods. +- **Custom metrics**: Extend `client/src/index.js` to publish additional data (GPIO readings, custom sensors, application logs, etc.). +- **Multiple namespaces**: Deploy the chart multiple times with different `MQTT_TOPIC_PREFIX` values. diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..f74f74a --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,167 @@ +# Quick Start Guide + +This guide walks you through running EdgeKit locally and then deploying it to a k3s cluster. + +--- + +## Running locally with Docker Compose + +### Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) ≥ 24 (with Compose v2) +- Git + +### Steps + +#### 1. Clone the repository + +```bash +git clone https://github.com/perspikapps/edgekit.git +cd edgekit +``` + +#### 2. (Optional) Build images manually + +```bash +./scripts/build.sh +``` + +#### 3. Start the full stack + +```bash +./scripts/start-local.sh +``` + +Expected output: + +``` +==> Starting edgekit stack (builds images if needed)… +[+] Building … +[+] Running 2/2 + ✔ Container edgekit-server Started + ✔ Container edgekit-client Started + +✅ edgekit is running! + + MQTT broker: mqtt://localhost:1883 + MQTT over WS: ws://localhost:9001 + + View logs: docker compose logs -f + Stop: ./scripts/stop-local.sh +``` + +#### 4. Verify metrics are flowing + +Open a second terminal and subscribe to all topics: + +```bash +docker run --rm --network host eclipse-mosquitto:2.0 \ + mosquitto_sub -h localhost -t "edgekit/#" -v +``` + +You should see JSON payloads arriving every 5 seconds: + +``` +edgekit/edge-local-1/metrics {"clientId":"edge-local-1","timestamp":"2024-01-15T10:30:00.000Z","cpu":{"loadPercent":5.12},...} +``` + +#### 5. Stop the stack + +```bash +./scripts/stop-local.sh +``` + +--- + +## Deploying on k3s + +### Prerequisites + +- A k3s cluster (single node is fine for testing) +- [Helm](https://helm.sh/docs/intro/install/) ≥ 3.14 +- `kubectl` configured for the cluster + +### Option A – Install from GHCR (recommended) + +```bash +helm install edgekit oci://ghcr.io/perspikapps/charts/edgekit \ + --namespace edgekit \ + --create-namespace \ + --wait +``` + +### Option B – Install from local chart + +```bash +helm install edgekit ./helm/edgekit \ + --namespace edgekit \ + --create-namespace \ + --wait +``` + +### Verify the deployment + +```bash +kubectl -n edgekit get pods +# NAME READY STATUS RESTARTS +# edgekit-server-xxxxxxxxxx-xxxxx 1/1 Running 0 +# edgekit-client-xxxxxxxxxx-xxxxx 1/1 Running 0 + +kubectl -n edgekit logs -f -l app.kubernetes.io/component=client +# [edgekit-client] Starting — id=edgekit-client-xxxxx broker=ws://edgekit-server:9001 +# [edgekit-client] Connected to ws://edgekit-server:9001 +# [edgekit-client] Published to edgekit/edgekit-client-xxxxx/metrics +``` + +### Scale to multiple clients + +```bash +helm upgrade edgekit ./helm/edgekit \ + --namespace edgekit \ + --set client.replicaCount=3 +``` + +### Change publish interval + +```bash +helm upgrade edgekit ./helm/edgekit \ + --namespace edgekit \ + --set client.publishIntervalMs=10000 +``` + +### Uninstall + +```bash +helm uninstall edgekit --namespace edgekit +kubectl delete namespace edgekit +``` + +--- + +## Subscribing to metrics from outside the cluster + +Forward the WebSocket port to your local machine: + +```bash +kubectl -n edgekit port-forward svc/edgekit-server 9001:9001 +``` + +Then connect any MQTT-over-WebSocket client to `ws://localhost:9001`. + +Example using `mosquitto_sub` with WebSocket support: + +```bash +docker run --rm --network host eclipse-mosquitto:2.0 \ + mosquitto_sub -h localhost -p 9001 -t "edgekit/#" -v +``` + +--- + +## Environment variable reference + +| Variable | Default | Description | +|---|---|---| +| `MQTT_BROKER_URL` | `ws://edgekit-server:9001` | Full WebSocket URL of the broker | +| `MQTT_TOPIC_PREFIX` | `edgekit` | Prefix for all published topics | +| `CLIENT_ID` | pod name (k8s) / `edge-local-1` (compose) | Unique agent identifier | +| `PUBLISH_INTERVAL_MS` | `5000` | Metrics publish interval (ms) | diff --git a/helm/edgekit/Chart.yaml b/helm/edgekit/Chart.yaml new file mode 100644 index 0000000..e985be8 --- /dev/null +++ b/helm/edgekit/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v2 +name: edgekit +description: > + EdgeKit – a client/server IoT edge platform running on k3s. + Deploys a central MQTT broker (server) and one or more edge agents (clients). +type: application +version: 0.1.0 +appVersion: "1.0.0" +keywords: + - iot + - mqtt + - edge + - k3s +home: https://github.com/perspikapps/edgekit +sources: + - https://github.com/perspikapps/edgekit +maintainers: + - name: edgekit maintainers + url: https://github.com/perspikapps/edgekit diff --git a/helm/edgekit/templates/NOTES.txt b/helm/edgekit/templates/NOTES.txt new file mode 100644 index 0000000..692ee8c --- /dev/null +++ b/helm/edgekit/templates/NOTES.txt @@ -0,0 +1,25 @@ +{{- if .Values.server.enabled }} +NOTES: +EdgeKit has been deployed! + +Server (MQTT Broker): + Service: {{ include "edgekit.server.fullname" . }} + MQTT (plain): {{ .Values.server.service.mqttPort }} + MQTT (WS): {{ .Values.server.service.wsPort }} + + To access the broker from within the cluster: + mqtt://{{ include "edgekit.server.fullname" . }}:{{ .Values.server.service.mqttPort }} + ws://{{ include "edgekit.server.fullname" . }}:{{ .Values.server.service.wsPort }} + +{{- if .Values.client.enabled }} +Client (Edge Agent): + Replicas: {{ .Values.client.replicaCount }} + Publishing to: {{ .Values.client.mqttBrokerUrl }} + Topic prefix: {{ .Values.client.topicPrefix }}//metrics + Interval: {{ .Values.client.publishIntervalMs }}ms +{{- end }} + +To watch logs: + kubectl logs -f -l app.kubernetes.io/component=server + kubectl logs -f -l app.kubernetes.io/component=client +{{- end }} diff --git a/helm/edgekit/templates/_helpers.tpl b/helm/edgekit/templates/_helpers.tpl new file mode 100644 index 0000000..8f35f76 --- /dev/null +++ b/helm/edgekit/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "edgekit.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "edgekit.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "edgekit.labels" -}} +helm.sh/chart: {{ include "edgekit.chart" . }} +{{ include "edgekit.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "edgekit.selectorLabels" -}} +app.kubernetes.io/name: {{ include "edgekit.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Chart label +*/}} +{{- define "edgekit.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Server full name +*/}} +{{- define "edgekit.server.fullname" -}} +{{- printf "%s-server" (include "edgekit.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Client full name +*/}} +{{- define "edgekit.client.fullname" -}} +{{- printf "%s-client" (include "edgekit.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} diff --git a/helm/edgekit/templates/client-deployment.yaml b/helm/edgekit/templates/client-deployment.yaml new file mode 100644 index 0000000..ca3ebea --- /dev/null +++ b/helm/edgekit/templates/client-deployment.yaml @@ -0,0 +1,54 @@ +{{- if .Values.client.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "edgekit.client.fullname" . }} + labels: + {{- include "edgekit.labels" . | nindent 4 }} + app.kubernetes.io/component: client +spec: + replicas: {{ .Values.client.replicaCount }} + selector: + matchLabels: + {{- include "edgekit.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: client + template: + metadata: + labels: + {{- include "edgekit.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: client + {{- with .Values.client.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + containers: + - name: edge-agent + image: "{{ .Values.client.image.repository }}:{{ .Values.client.image.tag }}" + imagePullPolicy: {{ .Values.client.image.pullPolicy }} + env: + - name: MQTT_BROKER_URL + value: {{ .Values.client.mqttBrokerUrl | quote }} + - name: MQTT_TOPIC_PREFIX + value: {{ .Values.client.topicPrefix | quote }} + - name: PUBLISH_INTERVAL_MS + value: {{ .Values.client.publishIntervalMs | quote }} + - name: CLIENT_ID + valueFrom: + fieldRef: + fieldPath: metadata.name + resources: + {{- toYaml .Values.client.resources | nindent 12 }} + {{- with .Values.client.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.client.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.client.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/helm/edgekit/templates/server-deployment.yaml b/helm/edgekit/templates/server-deployment.yaml new file mode 100644 index 0000000..3acdef9 --- /dev/null +++ b/helm/edgekit/templates/server-deployment.yaml @@ -0,0 +1,71 @@ +{{- if .Values.server.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "edgekit.server.fullname" . }} + labels: + {{- include "edgekit.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + replicas: {{ .Values.server.replicaCount }} + selector: + matchLabels: + {{- include "edgekit.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: server + template: + metadata: + labels: + {{- include "edgekit.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: server + {{- with .Values.server.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + containers: + - name: mosquitto + image: "{{ .Values.server.image.repository }}:{{ .Values.server.image.tag }}" + imagePullPolicy: {{ .Values.server.image.pullPolicy }} + ports: + - name: mqtt + containerPort: 1883 + protocol: TCP + - name: websockets + containerPort: 9001 + protocol: TCP + livenessProbe: + tcpSocket: + port: mqtt + initialDelaySeconds: 10 + periodSeconds: 20 + readinessProbe: + tcpSocket: + port: mqtt + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + {{- toYaml .Values.server.resources | nindent 12 }} + volumeMounts: + - name: mosquitto-data + mountPath: /mosquitto/data + volumes: + - name: mosquitto-data + {{- if .Values.server.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "edgekit.server.fullname" . }}-data + {{- else }} + emptyDir: {} + {{- end }} + {{- with .Values.server.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.server.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.server.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/helm/edgekit/templates/server-pvc.yaml b/helm/edgekit/templates/server-pvc.yaml new file mode 100644 index 0000000..9690fc6 --- /dev/null +++ b/helm/edgekit/templates/server-pvc.yaml @@ -0,0 +1,18 @@ +{{- if and .Values.server.enabled .Values.server.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "edgekit.server.fullname" . }}-data + labels: + {{- include "edgekit.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + accessModes: + - {{ .Values.server.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.server.persistence.size }} + {{- if .Values.server.persistence.storageClass }} + storageClassName: {{ .Values.server.persistence.storageClass }} + {{- end }} +{{- end }} diff --git a/helm/edgekit/templates/server-service.yaml b/helm/edgekit/templates/server-service.yaml new file mode 100644 index 0000000..4fe60ee --- /dev/null +++ b/helm/edgekit/templates/server-service.yaml @@ -0,0 +1,23 @@ +{{- if .Values.server.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "edgekit.server.fullname" . }} + labels: + {{- include "edgekit.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + type: {{ .Values.server.service.type }} + selector: + {{- include "edgekit.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: server + ports: + - name: mqtt + port: {{ .Values.server.service.mqttPort }} + targetPort: mqtt + protocol: TCP + - name: websockets + port: {{ .Values.server.service.wsPort }} + targetPort: websockets + protocol: TCP +{{- end }} diff --git a/helm/edgekit/values.yaml b/helm/edgekit/values.yaml new file mode 100644 index 0000000..a644f3e --- /dev/null +++ b/helm/edgekit/values.yaml @@ -0,0 +1,79 @@ +# ============================================================================= +# EdgeKit Helm Chart – default values +# ============================================================================= + +# -- Global image registry prefix (optional) +global: + imageRegistry: "" + +# ============================================================================= +# Server – Eclipse Mosquitto MQTT broker +# ============================================================================= +server: + enabled: true + + image: + repository: ghcr.io/perspikapps/edgekit-server + tag: "latest" + pullPolicy: IfNotPresent + + replicaCount: 1 + + service: + type: ClusterIP + mqttPort: 1883 + wsPort: 9001 + + resources: + requests: + cpu: 100m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + + persistence: + enabled: true + storageClass: "" + accessMode: ReadWriteOnce + size: 1Gi + + podAnnotations: {} + nodeSelector: {} + tolerations: [] + affinity: {} + +# ============================================================================= +# Client – Edge agent +# ============================================================================= +client: + enabled: true + + image: + repository: ghcr.io/perspikapps/edgekit-client + tag: "latest" + pullPolicy: IfNotPresent + + # Number of edge agent replicas (one per logical edge site) + replicaCount: 1 + + # MQTT broker URL – defaults to the in-cluster server service + mqttBrokerUrl: "ws://edgekit-server:9001" + + topicPrefix: "edgekit" + + # Publish interval in milliseconds + publishIntervalMs: 5000 + + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + + podAnnotations: {} + nodeSelector: {} + tolerations: [] + affinity: {} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..80b217b --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# ============================================================================= +# scripts/build.sh – Build all edgekit Docker images +# ============================================================================= +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TAG="${IMAGE_TAG:-local}" + +echo "==> Building edgekit-server (tag: ${TAG})" +docker build \ + --tag "edgekit-server:${TAG}" \ + --file "${REPO_ROOT}/server/Dockerfile" \ + "${REPO_ROOT}/server" + +echo "==> Building edgekit-client (tag: ${TAG})" +docker build \ + --tag "edgekit-client:${TAG}" \ + --file "${REPO_ROOT}/client/Dockerfile" \ + "${REPO_ROOT}/client" + +echo "" +echo "✅ All images built successfully." +echo " edgekit-server:${TAG}" +echo " edgekit-client:${TAG}" diff --git a/scripts/start-local.sh b/scripts/start-local.sh new file mode 100755 index 0000000..01d4320 --- /dev/null +++ b/scripts/start-local.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# ============================================================================= +# scripts/start-local.sh – Start the full edgekit stack locally via Docker Compose +# ============================================================================= +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +echo "==> Starting edgekit stack (builds images if needed)…" +docker compose -f "${REPO_ROOT}/docker-compose.yml" up --build -d + +echo "" +echo "✅ edgekit is running!" +echo "" +echo " MQTT broker: mqtt://localhost:1883" +echo " MQTT over WS: ws://localhost:9001" +echo "" +echo " View logs: docker compose logs -f" +echo " Stop: ./scripts/stop-local.sh" diff --git a/scripts/stop-local.sh b/scripts/stop-local.sh new file mode 100755 index 0000000..54a22e2 --- /dev/null +++ b/scripts/stop-local.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# ============================================================================= +# scripts/stop-local.sh – Stop the local edgekit Docker Compose stack +# ============================================================================= +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +echo "==> Stopping edgekit stack…" +docker compose -f "${REPO_ROOT}/docker-compose.yml" down + +echo "✅ edgekit stopped." diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..ddf2db8 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,15 @@ +FROM eclipse-mosquitto:2.0 + +LABEL org.opencontainers.image.title="edgekit-server" +LABEL org.opencontainers.image.description="Central MQTT broker for edgekit (WebSocket + plain MQTT)" +LABEL org.opencontainers.image.source="https://github.com/perspikapps/edgekit" + +COPY mosquitto.conf /mosquitto/config/mosquitto.conf +COPY entrypoint.sh /entrypoint.sh + +RUN chmod +x /entrypoint.sh + +EXPOSE 1883 9001 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["/usr/sbin/mosquitto", "-c", "/mosquitto/config/mosquitto.conf"] diff --git a/server/entrypoint.sh b/server/entrypoint.sh new file mode 100644 index 0000000..5bec99e --- /dev/null +++ b/server/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +# Ensure data and log directories exist and are writable +mkdir -p /mosquitto/data /mosquitto/log +chown -R mosquitto:mosquitto /mosquitto/data /mosquitto/log 2>/dev/null || true + +exec "$@" diff --git a/server/mosquitto.conf b/server/mosquitto.conf new file mode 100644 index 0000000..6307b79 --- /dev/null +++ b/server/mosquitto.conf @@ -0,0 +1,19 @@ +# ============================================================================= +# Mosquitto MQTT Broker Configuration +# edgekit-server +# ============================================================================= + +# Persistence +persistence true +persistence_location /mosquitto/data/ +log_dest stdout + +# --- Plain MQTT listener (internal cluster use) --- +listener 1883 +protocol mqtt +allow_anonymous true + +# --- WebSocket listener (for browser / WS clients) --- +listener 9001 +protocol websockets +allow_anonymous true From 1f5f346ea5d7602e7f04aabb0bba401dfc85cd54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 07:22:15 +0000 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20add=20develop=20branch=20support=20?= =?UTF-8?q?=E2=80=93=20CI=20triggers,=20branching=20strategy=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/perspikapps/edgekit/sessions/d3e01d94-d283-4ab5-b3a7-34dd055df0ae Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- README.md | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 587588b..4ed667f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: ["main"] + branches: ["develop", "main"] pull_request: - branches: ["main"] + branches: ["develop", "main"] jobs: lint-helm: diff --git a/README.md b/README.md index bf6ae65..f5fd447 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ edgekit/ │ └── stop-local.sh # Stop local stack ├── .github/ │ └── workflows/ -│ ├── ci.yml # CI: lint + build on PR +│ ├── ci.yml # CI: lint + build on PR/push to develop|main │ └── release.yml # Release: push images + Helm chart on tag ├── docs/ │ ├── architecture.md @@ -171,7 +171,7 @@ edgekit/ | Workflow | Trigger | What it does | |---|---|---| -| `ci.yml` | push / PR to `main` | Lints Helm chart, builds both Docker images | +| `ci.yml` | push / PR to `develop` or `main` | Lints Helm chart, builds both Docker images | | `release.yml` | push of `v*.*.*` tag | Builds & pushes images to GHCR, packages & pushes Helm chart to GHCR OCI registry | To release a new version: @@ -183,6 +183,16 @@ git push origin v1.0.0 --- +## Branching strategy + +| Branch | Purpose | +|---|---| +| `main` | Production-ready releases only | +| `develop` | Integration branch – all feature branches merge here | +| `feature/*` | Individual features or fixes | + +--- + ## Contributing Pull requests are welcome. Please open an issue first to discuss what you would like to change.