diff --git a/README.md b/README.md index 8687285..4d20a17 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Getting started with multicontainer on balena -This example will get you up and running quickly with a multicontainer setup on balena. The application creates a plot of your device's CPU load average and memory usage at the device's public URL, which is piped over using websockets. The system is composed of a simple static site server, a websocket server, and a proxy. These 3 components are defined in the [docker-compose.yml](docker-compose.yml) as services and are only given as much privilege as is needed. +This example will get you up and running quickly with a multicontainer setup on balena. The application creates a simple chart of your device's CPU usage at the device's public URL (and also shows the 1-minute load average). The system is composed of a Python data API, a Streamlit frontend, and a proxy. These 3 components are defined in the [docker-compose.yml](docker-compose.yml) as services and are only given as much privilege as is needed. + +Because `depends_on` is not supported by balena, startup ordering is handled with shared `flock` lock files on a named volume. Each container has a small bash startup script that waits for required dependency locks before starting. To get this project up and running, you'll need to [sign up](https://dashboard.balena-cloud.com/signup) for a balena account, create a microservices or starter application, and provision a device (device specific instructions can be found in our [getting started guide](https://balena.io/docs/getting-started). @@ -10,12 +12,8 @@ Once you are set up, clone this repo locally: ``` $ git clone git@github.com:balena-io-projects/multicontainer-getting-started.git ``` -Copy the command in the upper-right corner of your application dashboard to add your remote repository: -``` -$ git remote add balena username@git.balena-cloud.com:username/myapp.git -``` -Finally, push the code to the newly added remote: +From the project directory, push the code to your balena application with the balena CLI: ``` -$ git push balena master +$ balena push / ``` It should take a few minutes for your project to build. While you wait, you can enable device URLs, so you can see the server outside of our local network. This option can be found in the *Actions* drop down in your device dashboard. diff --git a/data/Dockerfile.template b/data/Dockerfile.template index 3ae94a2..b20c155 100644 --- a/data/Dockerfile.template +++ b/data/Dockerfile.template @@ -1,16 +1,12 @@ -FROM balenalib/%%BALENA_ARCH%%-debian-node:14-bullseye-run +FROM python:3.11-alpine -# Defines our working directory in container WORKDIR /usr/src/app -# Copies the package.json first for better cache on later pushes -COPY package.json package.json +RUN apk add --no-cache bash util-linux + +COPY requirements.txt requirements.txt +RUN pip install --no-cache-dir -r requirements.txt -# This install npm dependencies on the resin.io build server, -# making sure to clean up the artifacts it creates in order to reduce the image size. -RUN JOBS=MAX npm install --production -# This will copy all files in our root to the working directory in the container COPY . ./ -# server.js will run when container starts up on the device -CMD ["npm", "start"] +CMD ["bash", "./start.sh"] diff --git a/data/app.py b/data/app.py new file mode 100644 index 0000000..80b6974 --- /dev/null +++ b/data/app.py @@ -0,0 +1,58 @@ +import time +from flask import Flask, jsonify + +app = Flask(__name__) + + +def _read_proc_cpu_totals(): + with open("/proc/stat", "r", encoding="utf-8") as proc_stat: + cpu_line = proc_stat.readline().strip() + + parts = cpu_line.split() + counters = [int(value) for value in parts[1:]] + + idle = counters[3] + iowait = counters[4] if len(counters) > 4 else 0 + idle_all = idle + iowait + total = sum(counters) + return total, idle_all + + +def _cpu_percent_once(interval): + previous_total, previous_idle = _read_proc_cpu_totals() + time.sleep(interval) + current_total, current_idle = _read_proc_cpu_totals() + + total_delta = current_total - previous_total + idle_delta = current_idle - previous_idle + + if total_delta <= 0: + return 0.0 + + busy_delta = total_delta - idle_delta + return (busy_delta / total_delta) * 100.0 + + +def _cpu_percent(interval=0.5, samples=5): + if samples <= 0: + return 0.0 + + sample_interval = interval / samples + sample_values = [_cpu_percent_once(sample_interval) for _ in range(samples)] + return sum(sample_values) / len(sample_values) + + +def _load_1m(): + with open("/proc/loadavg", "r", encoding="utf-8") as proc_loadavg: + return float(proc_loadavg.readline().split()[0]) + + +@app.get("/cpu") +def cpu_usage(): + usage_percent = _cpu_percent(interval=0.5, samples=5) + one_minute_load = _load_1m() + return jsonify({"cpu_percent": usage_percent, "load_1m": one_minute_load}) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8080) diff --git a/data/index.js b/data/index.js deleted file mode 100644 index 8040a95..0000000 --- a/data/index.js +++ /dev/null @@ -1,57 +0,0 @@ -const express = require('express'); -const app = express(); -const server = require('http').createServer(app); -const io = require('socket.io')(server); -const { - exec -} = require('child_process'); - -server.listen(8080); - -const getCpuLoad = (socket) => { - exec('cat /proc/loadavg', (err, text) => { - if (err) { - throw err; - } - // Get overall average from last minute - const matchLoad = text.match(/(\d+\.\d+)\s+/); - if (matchLoad) { - const load = parseFloat(matchLoad[1]); - socket.emit('loadavg', { - onemin: load - }); - } - }); -}; - -const getMemoryInfo = (socket) => { - exec('cat /proc/meminfo', (err, text) => { - if (err) { - throw err; - } - // Get overall average from last minute - const matchTotal = text.match(/MemTotal:\s+([0-9]+)/); - const matchFree = text.match(/MemFree:\s+([0-9]+)/); - if (matchTotal && matchFree) { - const total = parseInt(matchTotal[1], 10); - const free = parseInt(matchFree[1], 10); - const percentageUsed = (total - free) / total * 100; - socket.emit('memory', { - used: percentageUsed - }); - } - }); -}; - -io.on('connection', function(socket) { - 'use strict'; - console.log('a user connected'); - let dataLoop = setInterval(function() { - getCpuLoad(socket); - getMemoryInfo(socket); - }, 1000); - socket.on('disconnect', function() { - console.log('a user disconnected'); - clearInterval(dataLoop); - }); -}); diff --git a/data/package-lock.json b/data/package-lock.json deleted file mode 100644 index bd3aea8..0000000 --- a/data/package-lock.json +++ /dev/null @@ -1,669 +0,0 @@ -{ - "name": "resin-websocket", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" - }, - "dependencies": { - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "~1.33.0" - } - } - } - }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" - }, - "backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" - }, - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" - }, - "base64id": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", - "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" - }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "requires": { - "callsite": "1.0.0" - } - }, - "blob": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", - "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=" - }, - "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.1", - "http-errors": "~1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "~2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "~1.6.15" - } - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" - }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "engine.io": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.0.tgz", - "integrity": "sha512-mRbgmAtQ4GAlKwuPnnAvXXwdPhEx+jkc0OBCLrXuD/CRvwNK3AxRSnqK4FSqmAMRRHryVJP8TopOvmEaA64fKw==", - "requires": { - "accepts": "~1.3.4", - "base64id": "1.0.0", - "cookie": "0.3.1", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.0", - "ws": "~3.3.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "engine.io-client": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", - "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", - "requires": { - "component-emitter": "1.2.1", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.1", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "ws": "~3.3.1", - "xmlhttprequest-ssl": "~1.5.4", - "yeast": "0.1.2" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "engine.io-parser": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz", - "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==", - "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.5", - "blob": "0.0.4", - "has-binary2": "~1.0.2" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", - "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", - "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.2", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.3", - "qs": "6.5.1", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "has-binary2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.2.tgz", - "integrity": "sha1-6D26SfC5vk0CbSc2U1DZ8D9Uvpg=", - "requires": { - "isarray": "2.0.1" - } - }, - "has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ipaddr.js": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", - "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "proxy-addr": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", - "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.6.0" - } - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "unpipe": "1.0.0" - }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": ">= 1.3.1 < 2" - } - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - } - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "socket.io": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.0.tgz", - "integrity": "sha512-KS+3CNWWNtLbVN5j0/B+1hjxRzey+oTK6ejpAOoxMZis6aXeB8cUtfuvjHl97tuZx+t/qD/VyqFMjuzu2Js6uQ==", - "requires": { - "debug": "~3.1.0", - "engine.io": "~3.2.0", - "has-binary2": "~1.0.2", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.1.0", - "socket.io-parser": "~3.2.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "socket.io-adapter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" - }, - "socket.io-client": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.0.tgz", - "integrity": "sha512-TvKPpL0cBON5LduQfR8Rxrr+ktj70bLXGvqHCL3er5avBXruB3gpnbaud5ikFYVfANH1gCABAvo0qN8Axpg2ew==", - "requires": { - "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", - "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "engine.io-client": "~3.2.0", - "has-binary2": "~1.0.2", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "socket.io-parser": "~3.2.0", - "to-array": "0.1.4" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "socket.io-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", - "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", - "requires": { - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "isarray": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.18" - }, - "dependencies": { - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "~1.33.0" - } - } - } - }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } - }, - "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" - }, - "yeast": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" - } - } -} diff --git a/data/package.json b/data/package.json deleted file mode 100644 index 6c0e139..0000000 --- a/data/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "resin-websocket", - "version": "1.0.1", - "description": "Websocket example", - "main": "index.js", - "homepage": "https://github.com/resin-io-projects/resin-websocket", - "repository": { - "type": "git", - "url": "git@github.com:resin-io-projects/resin-websocket.git" - }, - "scripts": { - "start": "node index.js" - }, - "author": "Kostas Lekkas ", - "license": "Apache-2.0", - "jshintConfig": { - "esversion": 6, - "strict": true - }, - "dependencies": { - "express": "^4.16.3", - "socket.io": "^2.1.0" - } -} diff --git a/data/requirements.txt b/data/requirements.txt new file mode 100644 index 0000000..0647450 --- /dev/null +++ b/data/requirements.txt @@ -0,0 +1 @@ +flask==3.0.3 diff --git a/data/start.sh b/data/start.sh new file mode 100644 index 0000000..a5d1dc6 --- /dev/null +++ b/data/start.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +LOCK_DIR="/shared-locks" +LOCK_FILE="$LOCK_DIR/data.lock" + +mkdir -p "$LOCK_DIR" +exec 9>"$LOCK_FILE" +flock -n 9 + +python app.py diff --git a/docker-compose.yml b/docker-compose.yml index 7b3b228..3e93fcc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,24 @@ -version: '2' +version: '2.1' + +volumes: + service-locks: + services: frontend: build: ./frontend + volumes: + - service-locks:/shared-locks expose: - "80" proxy: build: ./haproxy - depends_on: - - frontend - - data + volumes: + - service-locks:/shared-locks ports: - "80:80" data: build: ./data + volumes: + - service-locks:/shared-locks expose: - "8080" diff --git a/frontend/Dockerfile.template b/frontend/Dockerfile.template index 54715c7..ddc51f8 100644 --- a/frontend/Dockerfile.template +++ b/frontend/Dockerfile.template @@ -1,25 +1,14 @@ -FROM balenalib/%%BALENA_ARCH%%-debian-node:14-bullseye-run -# Defines our working directory in container -WORKDIR /usr/src/app +FROM python:3.11-alpine -RUN install_packages wget +WORKDIR /usr/src/app -ENV JQUERY_VERSION=3.3.1 -ENV REQUIREJS_VERSION=2.3.6 -ENV HIGHCHARTS_VERSION=7.0.3 -RUN mkdir -p static/js && \ - wget "https://code.jquery.com/jquery-${JQUERY_VERSION}.min.js" -O static/js/jquery.min.js && \ - wget "https://code.highcharts.com/${HIGHCHARTS_VERSION}/highcharts.js" -O static/js/highcharts.js && \ - wget "https://unpkg.com/requirejs@${REQUIREJS_VERSION}/require.js" -O static/js/require.js +RUN apk add --no-cache bash util-linux -# Copies the package.json first for better cache on later pushes -COPY package.json package.json +COPY requirements.txt requirements.txt +RUN pip install --no-cache-dir -r requirements.txt -# This install npm dependencies on the resin.io build server, -# making sure to clean up the artifacts it creates in order to reduce the image size. -RUN JOBS=MAX npm install --production -# This will copy all files in our root to the working directory in the container COPY . ./ -# server.js will run when container starts up on the device -CMD ["npm", "start"] +EXPOSE 80 + +CMD ["bash", "./start.sh"] diff --git a/frontend/app.py b/frontend/app.py new file mode 100644 index 0000000..ac636bf --- /dev/null +++ b/frontend/app.py @@ -0,0 +1,49 @@ +import time + +import pandas as pd +import requests +import streamlit as st + +st.set_page_config(page_title="CPU Usage", page_icon="CPU", layout="wide") +st.title("CPU Usage") + +history = st.session_state.setdefault("cpu_percent_history", []) +load_history = st.session_state.setdefault("load_1m_history", []) + +if st.button("Refresh"): + pass + +try: + response = requests.get("http://data:8080/cpu", timeout=2) + response.raise_for_status() + payload = response.json() + cpu_value = payload.get("cpu_percent") + load_value = payload.get("load_1m") + if cpu_value is not None: + history.append(float(cpu_value)) + if len(history) > 120: + del history[:-120] + if load_value is not None: + load_history.append(float(load_value)) + if len(load_history) > 120: + del load_history[:-120] +except requests.RequestException as exc: + st.error(f"Could not reach data service: {exc}") + +if history: + st.metric("CPU usage", f"{history[-1]:.1f}%") + chart_data = pd.DataFrame( + { + "sample": list(range(1, len(history) + 1)), + "cpu_percent": history, + } + ).set_index("sample") + st.line_chart(chart_data, use_container_width=True) + if load_history: + st.caption(f"1-minute load average: {load_history[-1]:.2f}") +else: + st.info("No CPU data yet.") + +# Keep the UI simple and alive without client-side JS. +time.sleep(2) +st.rerun() diff --git a/frontend/index.js b/frontend/index.js deleted file mode 100644 index d9a781c..0000000 --- a/frontend/index.js +++ /dev/null @@ -1,13 +0,0 @@ -var http = require('http'); -var express = require('express'); - -var PORT = 80; - -var app = express(); -var server = http.createServer(app); - -app.use(express.static(__dirname + '/static')) -server.listen(PORT, function() { - console.log("server is listening on port", PORT); -}); - diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 8a271f7..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "resin-websocket", - "version": "1.0.0", - "description": "Websocket example", - "main": "index.js", - "homepage": "https://github.com/resin-io-projects/resin-websocket", - "repository": { - "type": "git", - "url": "git@github.com:resin-io-projects/resin-websocket.git" - }, - "scripts": { - "start": "node index.js" - }, - "author": "Kostas Lekkas ", - "license": "Apache-2.0", - "dependencies": { - "express": "^4.16.2" - } -} diff --git a/frontend/requirements.txt b/frontend/requirements.txt new file mode 100644 index 0000000..07bb3b2 --- /dev/null +++ b/frontend/requirements.txt @@ -0,0 +1,3 @@ +pandas==2.2.3 +requests==2.32.3 +streamlit==1.40.2 diff --git a/frontend/start.sh b/frontend/start.sh new file mode 100644 index 0000000..faa9140 --- /dev/null +++ b/frontend/start.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +LOCK_DIR="/shared-locks" +DATA_LOCK="$LOCK_DIR/data.lock" +FRONTEND_LOCK="$LOCK_DIR/frontend.lock" + +mkdir -p "$LOCK_DIR" + +wait_for_running_lock() { + local lock_file="$1" + until [[ -f "$lock_file" ]] && ! flock -n "$lock_file" -c true; do + echo "Waiting for dependency lock: $lock_file" + sleep 1 + done +} + +wait_for_running_lock "$DATA_LOCK" + +exec 9>"$FRONTEND_LOCK" +flock -n 9 + +python -m streamlit run app.py --server.port 80 --server.address 0.0.0.0 diff --git a/frontend/static/index.html b/frontend/static/index.html deleted file mode 100644 index a5b32da..0000000 --- a/frontend/static/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - Live Data - - - -
- - diff --git a/frontend/static/js/app.js b/frontend/static/js/app.js deleted file mode 100644 index 22ad0de..0000000 --- a/frontend/static/js/app.js +++ /dev/null @@ -1,35 +0,0 @@ -requirejs.config({ - paths: { - jquery: "jquery.min", - highcharts: "highcharts", - socketio: "../socket.io/socket.io" - }, - shim: { - highcharts: { - exports: "Highcharts", - deps: ["jquery"] - }, - socketio: { - exports: "io" - } - } -}); - -// Retry failed module loading -function requireWithRetry(modules) { - var retryInterval = 5000; - var retryCount = 0; - var retryOnError = function(err) { - var failedId = err.requireModules && err.requireModules[0]; - // this is what tells RequireJS not to cache the previous failure status - requirejs.undef(failedId); - retryCount++; - console.log(`Retrying module loading #${retryCount} after wait...`); - setTimeout(function() { - requirejs([failedId], null, retryOnError); - }, retryInterval); - }; - requirejs(modules, null, retryOnError); -} -// Start our main code -requireWithRetry(["main"]); diff --git a/frontend/static/js/main.js b/frontend/static/js/main.js deleted file mode 100644 index c6f10d7..0000000 --- a/frontend/static/js/main.js +++ /dev/null @@ -1,87 +0,0 @@ -define(["jquery", "highcharts", "socketio"], function($, Highcharts, io) { - $(document).ready(function() { - var chart = new Highcharts.Chart({ - chart: { - zoomType: "xy", - margin: [80, 80, 80, 80], - renderTo: "container" - }, - title: { - text: "CPU Load Average & Memory Usage" - }, - subtitle: { - text: "Plotting CPU Load and Memory Info in real-time using websockets." - }, - xAxis: { - gridLineWidth: 5, - maxZoom: 60 - }, - yAxis: [ - { - title: { - text: "Memory %age" - }, - min: 0, - max: 100, - plotLines: [ - { - value: 0, - width: 1, - colour: "#808800" - } - ], - opposite: true - }, - { - title: { - text: "LoadAvg" - }, - min: 0, - plotLines: [ - { - value: 0, - width: 1, - colour: "#008888", - zIndex: 0 - } - ] - } - ], - plotOptions: { - column: { - pointPadding: 0, - groupPadding: 0 - } - }, - series: [ - { - name: "MemInfo", - type: "column", - color: "#008800", - grouping: false, - yAxis: 0, - data: [] - }, - { - name: "CpuLoad", - type: "spline", - yAxis: 1, - data: [] - } - ] - }); - - var socket = io.connect( - window.location.protocol + "//" + window.location.hostname - ); - - socket.on("loadavg", data => { - var series = chart.series[1]; - series.addPoint([data.onemin], true, series.data.length > 100); - }); - socket.on("memory", data => { - var series = chart.series[0]; - series.addPoint([data.used], true, series.data.length > 100); - }); - }); -}); diff --git a/haproxy/Dockerfile.aarch64 b/haproxy/Dockerfile.aarch64 index 00aca93..50dce51 100644 --- a/haproxy/Dockerfile.aarch64 +++ b/haproxy/Dockerfile.aarch64 @@ -1,3 +1,8 @@ FROM arm64v8/haproxy:1-alpine -COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg \ No newline at end of file +COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg +COPY start.sh /usr/local/bin/start.sh + +RUN apk add --no-cache bash util-linux + +CMD ["bash", "/usr/local/bin/start.sh"] \ No newline at end of file diff --git a/haproxy/Dockerfile.amd64 b/haproxy/Dockerfile.amd64 index 86379e9..1d6920a 100644 --- a/haproxy/Dockerfile.amd64 +++ b/haproxy/Dockerfile.amd64 @@ -1,3 +1,8 @@ FROM haproxy:1-alpine -COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg \ No newline at end of file +COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg +COPY start.sh /usr/local/bin/start.sh + +RUN apk add --no-cache bash util-linux + +CMD ["bash", "/usr/local/bin/start.sh"] \ No newline at end of file diff --git a/haproxy/Dockerfile.armhf b/haproxy/Dockerfile.armhf index 05868c2..2c8b986 100644 --- a/haproxy/Dockerfile.armhf +++ b/haproxy/Dockerfile.armhf @@ -1,3 +1,8 @@ FROM arm32v6/haproxy:1-alpine -COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg \ No newline at end of file +COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg +COPY start.sh /usr/local/bin/start.sh + +RUN apk add --no-cache bash util-linux + +CMD ["bash", "/usr/local/bin/start.sh"] \ No newline at end of file diff --git a/haproxy/Dockerfile.armv7hf b/haproxy/Dockerfile.armv7hf index 05868c2..2c8b986 100644 --- a/haproxy/Dockerfile.armv7hf +++ b/haproxy/Dockerfile.armv7hf @@ -1,3 +1,8 @@ FROM arm32v6/haproxy:1-alpine -COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg \ No newline at end of file +COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg +COPY start.sh /usr/local/bin/start.sh + +RUN apk add --no-cache bash util-linux + +CMD ["bash", "/usr/local/bin/start.sh"] \ No newline at end of file diff --git a/haproxy/Dockerfile.i386 b/haproxy/Dockerfile.i386 index 0687fd8..e849233 100644 --- a/haproxy/Dockerfile.i386 +++ b/haproxy/Dockerfile.i386 @@ -1,3 +1,8 @@ FROM i386/haproxy:1-alpine -COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg \ No newline at end of file +COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg +COPY start.sh /usr/local/bin/start.sh + +RUN apk add --no-cache bash util-linux + +CMD ["bash", "/usr/local/bin/start.sh"] \ No newline at end of file diff --git a/haproxy/Dockerfile.i386-nlp b/haproxy/Dockerfile.i386-nlp index 0687fd8..e849233 100644 --- a/haproxy/Dockerfile.i386-nlp +++ b/haproxy/Dockerfile.i386-nlp @@ -1,3 +1,8 @@ FROM i386/haproxy:1-alpine -COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg \ No newline at end of file +COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg +COPY start.sh /usr/local/bin/start.sh + +RUN apk add --no-cache bash util-linux + +CMD ["bash", "/usr/local/bin/start.sh"] \ No newline at end of file diff --git a/haproxy/Dockerfile.rpi b/haproxy/Dockerfile.rpi index 05868c2..2c8b986 100644 --- a/haproxy/Dockerfile.rpi +++ b/haproxy/Dockerfile.rpi @@ -1,3 +1,8 @@ FROM arm32v6/haproxy:1-alpine -COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg \ No newline at end of file +COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg +COPY start.sh /usr/local/bin/start.sh + +RUN apk add --no-cache bash util-linux + +CMD ["bash", "/usr/local/bin/start.sh"] \ No newline at end of file diff --git a/haproxy/haproxy.cfg b/haproxy/haproxy.cfg index 76106f8..9fe5f03 100644 --- a/haproxy/haproxy.cfg +++ b/haproxy/haproxy.cfg @@ -14,13 +14,6 @@ defaults frontend http bind *:80 default_backend backend_static_server - # Any URL beginning with socket.io will be flagged as 'is_websocket' - acl is_websocket path_beg /socket.io - acl is_websocket hdr(Upgrade) -i WebSocket - acl is_websocket hdr_beg(Host) -i ws - - # The connection to use if 'is_websocket' is flagged - use_backend backend_data if is_websocket backend backend_static_server mode http @@ -28,9 +21,3 @@ backend backend_static_server balance roundrobin timeout server 600s server static_1 frontend:80 check port 80 - -backend backend_data - balance source - option http-server-close - option forceclose - server api_1 data:8080 weight 1 maxconn 1024 check diff --git a/haproxy/start.sh b/haproxy/start.sh new file mode 100644 index 0000000..4f68faa --- /dev/null +++ b/haproxy/start.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +LOCK_DIR="/shared-locks" +FRONTEND_LOCK="$LOCK_DIR/frontend.lock" +PROXY_LOCK="$LOCK_DIR/proxy.lock" + +mkdir -p "$LOCK_DIR" + +wait_for_running_lock() { + local lock_file="$1" + until [[ -f "$lock_file" ]] && ! flock -n "$lock_file" -c true; do + echo "Waiting for dependency lock: $lock_file" + sleep 1 + done +} + +wait_for_running_lock "$FRONTEND_LOCK" + +exec 9>"$PROXY_LOCK" +flock -n 9 + +exec haproxy -f /usr/local/etc/haproxy/haproxy.cfg