Browse Source

Added docker-compose support

update-deps
Alex Mikhalev 7 years ago
parent
commit
9be28477ed
  1. 31
      Dockerfile
  2. 2
      app/pages/ProgramPage.tsx
  3. 4
      app/webpack.config.js
  4. 26
      build.sh
  5. 8
      common/Duration.ts
  6. 2
      common/env.js
  7. 2
      common/logger.ts
  8. 0
      common/paths.js
  9. 2
      common/sprinklersRpc/SprinklersDevice.ts
  10. 4
      common/sprinklersRpc/deviceRequests.ts
  11. 18
      common/sprinklersRpc/mqtt/MqttProgram.ts
  12. 18
      common/sprinklersRpc/mqtt/MqttSection.ts
  13. 14
      common/sprinklersRpc/mqtt/MqttSectionRunner.ts
  14. 60
      common/sprinklersRpc/mqtt/index.ts
  15. 8
      common/sprinklersRpc/schedule.ts
  16. 22
      docker-compose.yml
  17. 5
      package.json
  18. 2
      server/configureAlias.ts
  19. 2
      server/express/serveApp.ts
  20. 2
      server/index.ts
  21. 7
      tslint.json
  22. 2
      yarn.lock

31
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/ WORKDIR /app/
RUN yarn install --production
ADD dist/ /app/dist COPY --from=builder /app/package.json /app/yarn.lock ./
ADD public/ /app/public COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT [ "npm", "run", "start" ] ENTRYPOINT [ "npm", "run", "start" ]

2
app/pages/ProgramPage.tsx

@ -2,7 +2,7 @@ import { observer } from "mobx-react";
import { createViewModel, IViewModel } from "mobx-utils"; import { createViewModel, IViewModel } from "mobx-utils";
import * as React from "react"; import * as React from "react";
import { RouteComponentProps } from "react-router"; 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 { ProgramSequenceView, ScheduleView } from "@app/components";
import * as rp from "@app/routePaths"; import * as rp from "@app/routePaths";

4
app/webpack.config.js

@ -11,8 +11,8 @@ const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HappyPack = require("happypack"); const HappyPack = require("happypack");
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const {getClientEnvironment} = require("../env"); const {getClientEnvironment} = require("../common/env");
const paths = require("../paths"); const paths = require("../common/paths");
// Webpack uses `publicPath` to determine where the app is being served from. // 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. // It requires a trailing slash, or the file assets will get an incorrect path.

26
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" .

8
common/Duration.ts

@ -1,4 +1,8 @@
export class Duration { export class Duration {
static fromSeconds(seconds: number): Duration {
return new Duration(Math.floor(seconds / 60), seconds % 60);
}
minutes: number = 0; minutes: number = 0;
seconds: number = 0; seconds: number = 0;
@ -7,10 +11,6 @@ export class Duration {
this.seconds = seconds; this.seconds = seconds;
} }
static fromSeconds(seconds: number): Duration {
return new Duration(Math.floor(seconds / 60), seconds % 60);
}
toSeconds(): number { toSeconds(): number {
return this.minutes * 60 + this.seconds; return this.minutes * 60 + this.seconds;
} }

2
env.js → common/env.js

@ -13,7 +13,7 @@ if (!NODE_ENV) {
} }
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use // 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}.local`,
`${paths.dotenv}.${NODE_ENV}`, `${paths.dotenv}.${NODE_ENV}`,
// Don"t include `.env.local` for `test` environment // Don"t include `.env.local` for `test` environment

2
common/logger.ts

@ -1,5 +1,7 @@
import * as pino from "pino"; import * as pino from "pino";
// tslint:disable:no-console
type Level = "default" | "60" | "50" | "40" | "30" | "20" | "10"; type Level = "default" | "60" | "50" | "40" | "30" | "20" | "10";
const levels: {[level in Level]: string } = { const levels: {[level in Level]: string } = {

0
paths.js → common/paths.js

2
common/sprinklersRpc/SprinklersDevice.ts

@ -19,7 +19,7 @@ export abstract class SprinklersDevice {
sectionRunnerConstructor: typeof SectionRunner = SectionRunner; sectionRunnerConstructor: typeof SectionRunner = SectionRunner;
programConstructor: typeof Program = Program; programConstructor: typeof Program = Program;
constructor() { protected constructor() {
this.sectionRunner = new (this.sectionRunnerConstructor)(this); this.sectionRunner = new (this.sectionRunnerConstructor)(this);
} }

4
common/sprinklersRpc/deviceRequests.ts

@ -14,7 +14,7 @@ export type UpdateProgramResponse = Response<"updateProgram", { data: any }>;
export interface WithSection { sectionId: number; } export interface WithSection { sectionId: number; }
export type RunSectionData = WithSection & { duration: 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 RunSectionResponse = Response<"runSection", { runId: number }>;
export type CancelSectionRequest = WithSection & WithType<"cancelSection">; export type CancelSectionRequest = WithSection & WithType<"cancelSection">;
@ -26,7 +26,7 @@ export interface PauseSectionRunnerData { paused: boolean; }
export type PauseSectionRunnerRequest = PauseSectionRunnerData & WithType<"pauseSectionRunner">; export type PauseSectionRunnerRequest = PauseSectionRunnerData & WithType<"pauseSectionRunner">;
export type Request = RunProgramRequest | CancelProgramRequest | UpdateProgramRequest | export type Request = RunProgramRequest | CancelProgramRequest | UpdateProgramRequest |
RunSectionReqeust | CancelSectionRequest | CancelSectionRunIdRequest | PauseSectionRunnerRequest; RunSectionRequest | CancelSectionRequest | CancelSectionRunIdRequest | PauseSectionRunnerRequest;
export type RequestType = Request["type"]; export type RequestType = Request["type"];

18
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);
}
}

18
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);
}
}

14
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);
}
}

60
common/sprinklersRpc/mqtt/index.ts

@ -1,14 +1,16 @@
import { autorun, observable } from "mobx"; import { autorun, observable } from "mobx";
import * as mqtt from "mqtt"; import * as mqtt from "mqtt";
import { update } from "serializr";
import logger from "@common/logger"; import logger from "@common/logger";
import * as s from "@common/sprinklersRpc"; import * as s from "@common/sprinklersRpc";
import * as requests from "@common/sprinklersRpc/deviceRequests"; import * as requests from "@common/sprinklersRpc/deviceRequests";
import * as schema from "@common/sprinklersRpc/schema";
import { seralizeRequest } from "@common/sprinklersRpc/schema/requests"; import { seralizeRequest } from "@common/sprinklersRpc/schema/requests";
import { getRandomId } from "@common/utils"; import { getRandomId } from "@common/utils";
import { MqttProgram } from "./MqttProgram";
import { MqttSection } from "./MqttSection";
import { MqttSectionRunner } from "./MqttSectionRunner";
const log = logger.child({ source: "mqtt" }); const log = logger.child({ source: "mqtt" });
interface WithRid { interface WithRid {
@ -16,24 +18,24 @@ interface WithRid {
} }
export class MqttRpcClient implements s.SprinklersRPC { export class MqttRpcClient implements s.SprinklersRPC {
get connected(): boolean {
return this.connectionState.isServerConnected || false;
}
private static newClientId() {
return "sprinklers3-MqttApiClient-" + getRandomId();
}
readonly mqttUri: string; readonly mqttUri: string;
client!: mqtt.Client; client!: mqtt.Client;
@observable connectionState: s.ConnectionState = new s.ConnectionState(); @observable connectionState: s.ConnectionState = new s.ConnectionState();
devices: Map<string, MqttSprinklersDevice> = new Map(); devices: Map<string, MqttSprinklersDevice> = new Map();
get connected(): boolean {
return this.connectionState.isServerConnected || false;
}
constructor(mqttUri: string) { constructor(mqttUri: string) {
this.mqttUri = mqttUri; this.mqttUri = mqttUri;
this.connectionState.serverToBroker = false; this.connectionState.serverToBroker = false;
} }
private static newClientId() {
return "sprinklers3-MqttApiClient-" + getRandomId();
}
start() { start() {
const clientId = MqttRpcClient.newClientId(); const clientId = MqttRpcClient.newClientId();
log.info({ mqttUri: this.mqttUri, clientId }, "connecting to mqtt broker with client id"); 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; 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);
}
}

8
common/sprinklersRpc/schedule.ts

@ -1,6 +1,10 @@
import { observable } from "mobx"; import { observable } from "mobx";
export class TimeOfDay { export class TimeOfDay {
static fromDate(date: Date): TimeOfDay {
return new TimeOfDay(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
}
readonly hour: number; readonly hour: number;
readonly minute: number; readonly minute: number;
readonly second: number; readonly second: number;
@ -12,10 +16,6 @@ export class TimeOfDay {
this.second = second; this.second = second;
this.millisecond = millisecond; this.millisecond = millisecond;
} }
static fromDate(date: Date): TimeOfDay {
return new TimeOfDay(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
}
} }
export enum Weekday { export enum Weekday {

22
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

5
package.json

@ -8,6 +8,7 @@
"clean": "rm -rf ./dist ./build ./public", "clean": "rm -rf ./dist ./build ./public",
"build:app": "NODE_ENV=production webpack --config ./app/webpack.config.js --env prod", "build:app": "NODE_ENV=production webpack --config ./app/webpack.config.js --env prod",
"build:server": "tsc --project server", "build:server": "tsc --project server",
"build": "run-p build:*",
"watch:app": "yarn build:app --watch", "watch:app": "yarn build:app --watch",
"watch:server": "yarn build:server --watch", "watch:server": "yarn build:server --watch",
"start:dev-server": "NODE_ENV=development webpack-dev-server --config ./app/webpack.config.js --env dev", "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": "^4.16.3",
"express-pino-logger": "^3.0.2", "express-pino-logger": "^3.0.2",
"express-promise-router": "^3.0.3", "express-promise-router": "^3.0.3",
"fork-ts-checker-webpack-plugin": "^0.4.3",
"jsonwebtoken": "^8.3.0", "jsonwebtoken": "^8.3.0",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"mobx": "^5.0.3", "mobx": "^5.0.3",
@ -54,7 +54,6 @@
"reflect-metadata": "^0.1.12", "reflect-metadata": "^0.1.12",
"serializr": "^1.3.0", "serializr": "^1.3.0",
"typeorm": "^0.2.7", "typeorm": "^0.2.7",
"uglify-es": "3.3.9",
"ws": "^6.0.0" "ws": "^6.0.0"
}, },
"devDependencies": { "devDependencies": {
@ -84,6 +83,7 @@
"favicons-webpack-plugin": "^0.0.9", "favicons-webpack-plugin": "^0.0.9",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"fork-ts-checker-webpack-plugin": "^0.4.3",
"happypack": "^5.0.0", "happypack": "^5.0.0",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.4.1", "mini-css-extract-plugin": "^0.4.1",
@ -114,6 +114,7 @@
"tslint": "^5.11.0", "tslint": "^5.11.0",
"tslint-react": "^3.6.0", "tslint-react": "^3.6.0",
"typescript": "^2.9.2", "typescript": "^2.9.2",
"uglify-es": "^3.3.9",
"uglifyjs-webpack-plugin": "^1.2.6", "uglifyjs-webpack-plugin": "^1.2.6",
"url-loader": "^1.0.1", "url-loader": "^1.0.1",
"webpack": "^4.16.2", "webpack": "^4.16.2",

2
server/configureAlias.ts

@ -2,5 +2,3 @@ import * as moduleAlias from "module-alias";
import * as path from "path"; import * as path from "path";
moduleAlias.addAlias("@common", path.resolve(__dirname, "..", "common")); moduleAlias.addAlias("@common", path.resolve(__dirname, "..", "common"));
moduleAlias.addAlias("@server", __dirname); moduleAlias.addAlias("@server", __dirname);
moduleAlias.addAlias("env", require.resolve("../env"));
moduleAlias.addAlias("paths", require.resolve("../paths"));

2
server/express/serveApp.ts

@ -2,7 +2,7 @@ import { Express } from "express";
import * as path from "path"; import * as path from "path";
import * as serveStatic from "serve-static"; 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"); const index = path.join(paths.publicDir, "index.html");

2
server/index.ts

@ -1,7 +1,7 @@
/* tslint:disable:ordered-imports */ /* tslint:disable:ordered-imports */
import "reflect-metadata"; import "reflect-metadata";
import "./configureAlias"; import "./configureAlias";
import "env"; import "@common/env";
import "./configureLogger"; import "./configureLogger";
import log from "@common/logger"; import log from "@common/logger";

7
tslint.json

@ -9,7 +9,7 @@
true true
], ],
"max-classes-per-file": [ "max-classes-per-file": [
false true, 3
], ],
"ordered-imports": true, "ordered-imports": true,
"variable-name": [ "variable-name": [
@ -24,16 +24,13 @@
"member-ordering": [ "member-ordering": [
true, true,
{ {
"order": "fields-first" "order": "statics-first"
} }
], ],
"object-literal-sort-keys": [ "object-literal-sort-keys": [
false false
], ],
"no-submodule-imports": false, "no-submodule-imports": false,
"no-unused-variable": [
true
],
"jsx-boolean-value": [ true, "never" ], "jsx-boolean-value": [ true, "never" ],
"no-implicit-dependencies": false "no-implicit-dependencies": false
}, },

2
yarn.lock

@ -7125,7 +7125,7 @@ ua-parser-js@^0.7.18:
version "0.7.18" version "0.7.18"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" 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" version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
dependencies: dependencies:

Loading…
Cancel
Save