Added docker-compose support
This commit is contained in:
		
							parent
							
								
									79ff1e8ff9
								
							
						
					
					
						commit
						9be28477ed
					
				
							
								
								
									
										31
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -1,13 +1,32 @@ | ||||
| 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 | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
| RUN npm install --global yarn | ||||
| 
 | ||||
| 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" ] | ||||
|  | ||||
| @ -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"; | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
							
								
								
									
										26
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										26
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							| @ -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" . | ||||
| @ -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; | ||||
|     } | ||||
|  | ||||
| @ -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
 | ||||
| @ -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 } = { | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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"]; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										18
									
								
								common/sprinklersRpc/mqtt/MqttProgram.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								common/sprinklersRpc/mqtt/MqttProgram.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								common/sprinklersRpc/mqtt/MqttSection.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								common/sprinklersRpc/mqtt/MqttSectionRunner.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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<string, MqttSprinklersDevice> = 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
							
								
								
									
										22
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| @ -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", | ||||
|  | ||||
| @ -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")); | ||||
|  | ||||
| @ -11,4 +11,4 @@ const errorHandler: express.ErrorRequestHandler = | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
| export default errorHandler; | ||||
| export default errorHandler; | ||||
|  | ||||
| @ -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"); | ||||
| 
 | ||||
|  | ||||
| @ -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"; | ||||
|  | ||||
| @ -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 | ||||
|   }, | ||||
|  | ||||
| @ -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: | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user