From 9be28477ed55ad776dd56108753ba228adbdf0f1 Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Wed, 25 Jul 2018 12:53:33 -0600 Subject: [PATCH] Added docker-compose support --- Dockerfile | 31 ++++++++-- app/pages/ProgramPage.tsx | 2 +- app/webpack.config.js | 4 +- build.sh | 26 ++++++++ common/Duration.ts | 8 +-- env.js => common/env.js | 2 +- common/logger.ts | 2 + paths.js => common/paths.js | 0 common/sprinklersRpc/SprinklersDevice.ts | 2 +- common/sprinklersRpc/deviceRequests.ts | 4 +- common/sprinklersRpc/mqtt/MqttProgram.ts | 18 ++++++ common/sprinklersRpc/mqtt/MqttSection.ts | 18 ++++++ .../sprinklersRpc/mqtt/MqttSectionRunner.ts | 14 +++++ common/sprinklersRpc/mqtt/index.ts | 60 ++++--------------- common/sprinklersRpc/schedule.ts | 8 +-- docker-compose.yml | 22 +++++++ package.json | 5 +- server/configureAlias.ts | 2 - server/express/errorHandler.ts | 2 +- server/express/serveApp.ts | 2 +- server/index.ts | 2 +- tslint.json | 7 +-- yarn.lock | 2 +- 23 files changed, 161 insertions(+), 82 deletions(-) create mode 100755 build.sh rename env.js => common/env.js (99%) rename paths.js => common/paths.js (100%) create mode 100644 common/sprinklersRpc/mqtt/MqttProgram.ts create mode 100644 common/sprinklersRpc/mqtt/MqttSection.ts create mode 100644 common/sprinklersRpc/mqtt/MqttSectionRunner.ts create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile index df50eca..36e7d7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,32 @@ -FROM node:alpine +FROM node:alpine as builder + +RUN apk add yarn \ + python \ + make \ + g++ + +WORKDIR /app/ + +COPY package.json yarn.lock /app/ +RUN yarn install --frozen-lockfile -RUN npm install --global yarn +COPY tslint.json /app +COPY app/ /app/app +COPY common/ /app/common +COPY server/ /app/server + +RUN yarn build + +RUN yarn install --frozen-lockfile --production + +FROM node:alpine -ADD package.json yarn.lock /app/ WORKDIR /app/ -RUN yarn install --production -ADD dist/ /app/dist -ADD public/ /app/public +COPY --from=builder /app/package.json /app/yarn.lock ./ +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/public ./public EXPOSE 8080 ENTRYPOINT [ "npm", "run", "start" ] diff --git a/app/pages/ProgramPage.tsx b/app/pages/ProgramPage.tsx index c7311c4..78652f0 100644 --- a/app/pages/ProgramPage.tsx +++ b/app/pages/ProgramPage.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; import { createViewModel, IViewModel } from "mobx-utils"; import * as React from "react"; import { RouteComponentProps } from "react-router"; -import { Button, CheckboxProps, Form, Input, InputOnChangeData, Menu, Modal, Segment } from "semantic-ui-react"; +import { Button, CheckboxProps, Form, Input, InputOnChangeData, Menu, Modal } from "semantic-ui-react"; import { ProgramSequenceView, ScheduleView } from "@app/components"; import * as rp from "@app/routePaths"; diff --git a/app/webpack.config.js b/app/webpack.config.js index 724af23..177e22f 100644 --- a/app/webpack.config.js +++ b/app/webpack.config.js @@ -11,8 +11,8 @@ const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const HappyPack = require("happypack"); const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); -const {getClientEnvironment} = require("../env"); -const paths = require("../paths"); +const {getClientEnvironment} = require("../common/env"); +const paths = require("../common/paths"); // Webpack uses `publicPath` to determine where the app is being served from. // It requires a trailing slash, or the file assets will get an incorrect path. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..82739b8 --- /dev/null +++ b/build.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -e + +IMAGE_NAME="amikhalev/sprinklers3" +BUILD_IMAGE="$IMAGE_NAME:build" +DIST_IMAGE="$IMAGE_NAME:dist" +EXTRACT_CONTAINER="extract-$RANDOM" +BUILD_DIR="." + +echo "Cleaning build files" +rm -rf ./build ./dist ./public + +echo "Building build image $BUILD_IMAGE" +docker build -t "$BUILD_IMAGE" --target builder . + +echo "Extracting build image using container $EXTRACT_CONTAINER" +mkdir -p ./build +cp package.json yarn.lock "$BUILD_DIR" +docker container create --name "$EXTRACT_CONTAINER" "$BUILD_IMAGE" +docker container cp "$EXTRACT_CONTAINER:/app/dist" "$BUILD_DIR/dist" +docker container cp "$EXTRACT_CONTAINER:/app/public" "$BUILD_DIR/public" +docker container rm -f "$EXTRACT_CONTAINER" + +echo "Building dist image $DIST_IMAGE" +docker build -t "$DIST_IMAGE" . \ No newline at end of file diff --git a/common/Duration.ts b/common/Duration.ts index 9d4ed01..1f86fd4 100644 --- a/common/Duration.ts +++ b/common/Duration.ts @@ -1,4 +1,8 @@ export class Duration { + static fromSeconds(seconds: number): Duration { + return new Duration(Math.floor(seconds / 60), seconds % 60); + } + minutes: number = 0; seconds: number = 0; @@ -7,10 +11,6 @@ export class Duration { this.seconds = seconds; } - static fromSeconds(seconds: number): Duration { - return new Duration(Math.floor(seconds / 60), seconds % 60); - } - toSeconds(): number { return this.minutes * 60 + this.seconds; } diff --git a/env.js b/common/env.js similarity index 99% rename from env.js rename to common/env.js index c29b7e0..8b66094 100644 --- a/env.js +++ b/common/env.js @@ -13,7 +13,7 @@ if (!NODE_ENV) { } // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use -var dotenvFiles = [ +const dotenvFiles = [ `${paths.dotenv}.${NODE_ENV}.local`, `${paths.dotenv}.${NODE_ENV}`, // Don"t include `.env.local` for `test` environment diff --git a/common/logger.ts b/common/logger.ts index 357c5cf..a1c1f7a 100644 --- a/common/logger.ts +++ b/common/logger.ts @@ -1,5 +1,7 @@ import * as pino from "pino"; +// tslint:disable:no-console + type Level = "default" | "60" | "50" | "40" | "30" | "20" | "10"; const levels: {[level in Level]: string } = { diff --git a/paths.js b/common/paths.js similarity index 100% rename from paths.js rename to common/paths.js diff --git a/common/sprinklersRpc/SprinklersDevice.ts b/common/sprinklersRpc/SprinklersDevice.ts index 0ef6162..666a0dc 100644 --- a/common/sprinklersRpc/SprinklersDevice.ts +++ b/common/sprinklersRpc/SprinklersDevice.ts @@ -19,7 +19,7 @@ export abstract class SprinklersDevice { sectionRunnerConstructor: typeof SectionRunner = SectionRunner; programConstructor: typeof Program = Program; - constructor() { + protected constructor() { this.sectionRunner = new (this.sectionRunnerConstructor)(this); } diff --git a/common/sprinklersRpc/deviceRequests.ts b/common/sprinklersRpc/deviceRequests.ts index 056a725..143b159 100644 --- a/common/sprinklersRpc/deviceRequests.ts +++ b/common/sprinklersRpc/deviceRequests.ts @@ -14,7 +14,7 @@ export type UpdateProgramResponse = Response<"updateProgram", { data: any }>; export interface WithSection { sectionId: number; } export type RunSectionData = WithSection & { duration: number }; -export type RunSectionReqeust = RunSectionData & WithType<"runSection">; +export type RunSectionRequest = RunSectionData & WithType<"runSection">; export type RunSectionResponse = Response<"runSection", { runId: number }>; export type CancelSectionRequest = WithSection & WithType<"cancelSection">; @@ -26,7 +26,7 @@ export interface PauseSectionRunnerData { paused: boolean; } export type PauseSectionRunnerRequest = PauseSectionRunnerData & WithType<"pauseSectionRunner">; export type Request = RunProgramRequest | CancelProgramRequest | UpdateProgramRequest | - RunSectionReqeust | CancelSectionRequest | CancelSectionRunIdRequest | PauseSectionRunnerRequest; + RunSectionRequest | CancelSectionRequest | CancelSectionRunIdRequest | PauseSectionRunnerRequest; export type RequestType = Request["type"]; diff --git a/common/sprinklersRpc/mqtt/MqttProgram.ts b/common/sprinklersRpc/mqtt/MqttProgram.ts new file mode 100644 index 0000000..759254d --- /dev/null +++ b/common/sprinklersRpc/mqtt/MqttProgram.ts @@ -0,0 +1,18 @@ +import { update } from "serializr"; + +import * as s from "@common/sprinklersRpc"; +import * as schema from "@common/sprinklersRpc/schema"; + +export class MqttProgram extends s.Program { + onMessage(payload: string, topic: string | undefined) { + if (topic === "running") { + this.running = (payload === "true"); + } else if (topic == null) { + this.updateFromJSON(JSON.parse(payload)); + } + } + + updateFromJSON(json: any) { + update(schema.program, this, json); + } +} diff --git a/common/sprinklersRpc/mqtt/MqttSection.ts b/common/sprinklersRpc/mqtt/MqttSection.ts new file mode 100644 index 0000000..a8eb071 --- /dev/null +++ b/common/sprinklersRpc/mqtt/MqttSection.ts @@ -0,0 +1,18 @@ +import { update } from "serializr"; + +import * as s from "@common/sprinklersRpc"; +import * as schema from "@common/sprinklersRpc/schema"; + +export class MqttSection extends s.Section { + onMessage(payload: string, topic: string | undefined) { + if (topic === "state") { + this.state = (payload === "true"); + } else if (topic == null) { + this.updateFromJSON(JSON.parse(payload)); + } + } + + updateFromJSON(json: any) { + update(schema.section, this, json); + } +} diff --git a/common/sprinklersRpc/mqtt/MqttSectionRunner.ts b/common/sprinklersRpc/mqtt/MqttSectionRunner.ts new file mode 100644 index 0000000..e6e2dd6 --- /dev/null +++ b/common/sprinklersRpc/mqtt/MqttSectionRunner.ts @@ -0,0 +1,14 @@ +import { update } from "serializr"; + +import * as s from "@common/sprinklersRpc"; +import * as schema from "@common/sprinklersRpc/schema"; + +export class MqttSectionRunner extends s.SectionRunner { + onMessage(payload: string) { + this.updateFromJSON(JSON.parse(payload)); + } + + updateFromJSON(json: any) { + update(schema.sectionRunner, this, json); + } +} diff --git a/common/sprinklersRpc/mqtt/index.ts b/common/sprinklersRpc/mqtt/index.ts index e4d30ce..de59eab 100644 --- a/common/sprinklersRpc/mqtt/index.ts +++ b/common/sprinklersRpc/mqtt/index.ts @@ -1,14 +1,16 @@ import { autorun, observable } from "mobx"; import * as mqtt from "mqtt"; -import { update } from "serializr"; import logger from "@common/logger"; import * as s from "@common/sprinklersRpc"; import * as requests from "@common/sprinklersRpc/deviceRequests"; -import * as schema from "@common/sprinklersRpc/schema"; import { seralizeRequest } from "@common/sprinklersRpc/schema/requests"; import { getRandomId } from "@common/utils"; +import { MqttProgram } from "./MqttProgram"; +import { MqttSection } from "./MqttSection"; +import { MqttSectionRunner } from "./MqttSectionRunner"; + const log = logger.child({ source: "mqtt" }); interface WithRid { @@ -16,24 +18,24 @@ interface WithRid { } export class MqttRpcClient implements s.SprinklersRPC { + get connected(): boolean { + return this.connectionState.isServerConnected || false; + } + + private static newClientId() { + return "sprinklers3-MqttApiClient-" + getRandomId(); + } + readonly mqttUri: string; client!: mqtt.Client; @observable connectionState: s.ConnectionState = new s.ConnectionState(); devices: Map = new Map(); - get connected(): boolean { - return this.connectionState.isServerConnected || false; - } - constructor(mqttUri: string) { this.mqttUri = mqttUri; this.connectionState.serverToBroker = false; } - private static newClientId() { - return "sprinklers3-MqttApiClient-" + getRandomId(); - } - start() { const clientId = MqttRpcClient.newClientId(); log.info({ mqttUri: this.mqttUri, clientId }, "connecting to mqtt broker with client id"); @@ -281,41 +283,3 @@ class MqttSprinklersDevice extends s.SprinklersDevice { } type ResponseCallback = (response: requests.Response) => void; - -class MqttSection extends s.Section { - onMessage(payload: string, topic: string | undefined) { - if (topic === "state") { - this.state = (payload === "true"); - } else if (topic == null) { - this.updateFromJSON(JSON.parse(payload)); - } - } - - updateFromJSON(json: any) { - update(schema.section, this, json); - } -} - -class MqttProgram extends s.Program { - onMessage(payload: string, topic: string | undefined) { - if (topic === "running") { - this.running = (payload === "true"); - } else if (topic == null) { - this.updateFromJSON(JSON.parse(payload)); - } - } - - updateFromJSON(json: any) { - update(schema.program, this, json); - } -} - -class MqttSectionRunner extends s.SectionRunner { - onMessage(payload: string) { - this.updateFromJSON(JSON.parse(payload)); - } - - updateFromJSON(json: any) { - update(schema.sectionRunner, this, json); - } -} diff --git a/common/sprinklersRpc/schedule.ts b/common/sprinklersRpc/schedule.ts index a9cafe6..8f431c5 100644 --- a/common/sprinklersRpc/schedule.ts +++ b/common/sprinklersRpc/schedule.ts @@ -1,6 +1,10 @@ import { observable } from "mobx"; export class TimeOfDay { + static fromDate(date: Date): TimeOfDay { + return new TimeOfDay(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()); + } + readonly hour: number; readonly minute: number; readonly second: number; @@ -12,10 +16,6 @@ export class TimeOfDay { this.second = second; this.millisecond = millisecond; } - - static fromDate(date: Date): TimeOfDay { - return new TimeOfDay(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()); - } } export enum Weekday { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..316fd67 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: "3" +services: + web: + image: "amikhalev/sprinklers3" + build: . + ports: + - "8080:8080" + env_file: + - .env + environment: + - PORT=8080 + - TYPEORM_CONNECTION=postgres + - TYPEORM_HOST=database + - TYPEORM_DATABASE=postgres + - TYPEORM_USERNAME=postgres + - TYPEORM_PASSWORD=8JN4w0UsN5dbjMjNvPe452P2yYOqg5PV + # Must specify JWT_SECRET and MQTT_URL + + database: + image: "postgres:11-alpine" + environment: + - POSTGRES_PASSWORD=8JN4w0UsN5dbjMjNvPe452P2yYOqg5PV \ No newline at end of file diff --git a/package.json b/package.json index f0afde6..fbeaad1 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "clean": "rm -rf ./dist ./build ./public", "build:app": "NODE_ENV=production webpack --config ./app/webpack.config.js --env prod", "build:server": "tsc --project server", + "build": "run-p build:*", "watch:app": "yarn build:app --watch", "watch:server": "yarn build:server --watch", "start:dev-server": "NODE_ENV=development webpack-dev-server --config ./app/webpack.config.js --env dev", @@ -41,7 +42,6 @@ "express": "^4.16.3", "express-pino-logger": "^3.0.2", "express-promise-router": "^3.0.3", - "fork-ts-checker-webpack-plugin": "^0.4.3", "jsonwebtoken": "^8.3.0", "lodash": "^4.17.10", "mobx": "^5.0.3", @@ -54,7 +54,6 @@ "reflect-metadata": "^0.1.12", "serializr": "^1.3.0", "typeorm": "^0.2.7", - "uglify-es": "3.3.9", "ws": "^6.0.0" }, "devDependencies": { @@ -84,6 +83,7 @@ "favicons-webpack-plugin": "^0.0.9", "file-loader": "^1.1.11", "font-awesome": "^4.7.0", + "fork-ts-checker-webpack-plugin": "^0.4.3", "happypack": "^5.0.0", "html-webpack-plugin": "^3.2.0", "mini-css-extract-plugin": "^0.4.1", @@ -114,6 +114,7 @@ "tslint": "^5.11.0", "tslint-react": "^3.6.0", "typescript": "^2.9.2", + "uglify-es": "^3.3.9", "uglifyjs-webpack-plugin": "^1.2.6", "url-loader": "^1.0.1", "webpack": "^4.16.2", diff --git a/server/configureAlias.ts b/server/configureAlias.ts index 45250a6..98c8a45 100644 --- a/server/configureAlias.ts +++ b/server/configureAlias.ts @@ -2,5 +2,3 @@ import * as moduleAlias from "module-alias"; import * as path from "path"; moduleAlias.addAlias("@common", path.resolve(__dirname, "..", "common")); moduleAlias.addAlias("@server", __dirname); -moduleAlias.addAlias("env", require.resolve("../env")); -moduleAlias.addAlias("paths", require.resolve("../paths")); diff --git a/server/express/errorHandler.ts b/server/express/errorHandler.ts index c2de31b..494ce99 100644 --- a/server/express/errorHandler.ts +++ b/server/express/errorHandler.ts @@ -11,4 +11,4 @@ const errorHandler: express.ErrorRequestHandler = } }; -export default errorHandler; \ No newline at end of file +export default errorHandler; diff --git a/server/express/serveApp.ts b/server/express/serveApp.ts index 3ef546c..5884418 100644 --- a/server/express/serveApp.ts +++ b/server/express/serveApp.ts @@ -2,7 +2,7 @@ import { Express } from "express"; import * as path from "path"; import * as serveStatic from "serve-static"; -import * as paths from "paths"; +import * as paths from "@common/paths"; const index = path.join(paths.publicDir, "index.html"); diff --git a/server/index.ts b/server/index.ts index 4318003..cc22a84 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,7 +1,7 @@ /* tslint:disable:ordered-imports */ import "reflect-metadata"; import "./configureAlias"; -import "env"; +import "@common/env"; import "./configureLogger"; import log from "@common/logger"; diff --git a/tslint.json b/tslint.json index f13b273..8d4daa4 100644 --- a/tslint.json +++ b/tslint.json @@ -9,7 +9,7 @@ true ], "max-classes-per-file": [ - false + true, 3 ], "ordered-imports": true, "variable-name": [ @@ -24,16 +24,13 @@ "member-ordering": [ true, { - "order": "fields-first" + "order": "statics-first" } ], "object-literal-sort-keys": [ false ], "no-submodule-imports": false, - "no-unused-variable": [ - true - ], "jsx-boolean-value": [ true, "never" ], "no-implicit-dependencies": false }, diff --git a/yarn.lock b/yarn.lock index 9c931de..f7d7327 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7125,7 +7125,7 @@ ua-parser-js@^0.7.18: version "0.7.18" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" -uglify-es@3.3.9, uglify-es@^3.3.4: +uglify-es@^3.3.4, uglify-es@^3.3.9: version "3.3.9" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" dependencies: