From dfd7fd30deb9068b8222c55210dd1ea7c2f7436c Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Thu, 27 Dec 2018 20:33:40 -0800 Subject: [PATCH] skeleton for one click deploy is done. Untested. --- app-frontend/src/api/ApiManager.ts | 18 +++ .../apps/oneclick/OneClickAppConfigPage.tsx | 3 +- .../apps/oneclick/OneClickAppDeployHelper.ts | 33 +++++- .../oneclick/OneClickAppDeploymentHelper.ts | 111 ++++++++++++++++++ app-frontend/src/models/ICaptainDefinition.ts | 6 + 5 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 app-frontend/src/containers/apps/oneclick/OneClickAppDeploymentHelper.ts create mode 100644 app-frontend/src/models/ICaptainDefinition.ts diff --git a/app-frontend/src/api/ApiManager.ts b/app-frontend/src/api/ApiManager.ts index 3f74e90..c08fa00 100644 --- a/app-frontend/src/api/ApiManager.ts +++ b/app-frontend/src/api/ApiManager.ts @@ -3,6 +3,7 @@ import Logger from "../utils/Logger"; import { IAppDef } from "../containers/apps/AppDefinition"; import { IRegistryInfo } from "../models/IRegistryInfo"; import Utils from "../utils/Utils"; +import { ICaptainDefinition } from "../models/ICaptainDefinition"; const URL = process.env.REACT_APP_API_URL + "/api/v2"; Logger.dev("API URL: " + URL); @@ -94,6 +95,23 @@ export default class ApiManager { ); } + uploadCaptainDefinitionContent( + appName: string, + captainDefinition: ICaptainDefinition, + detached: boolean + ) { + const http = this.http; + + return Promise.resolve() // + .then( + http.fetch( + http.POST, + "/user/apps/appData/" + appName + (detached ? "?detached=1" : ""), + { captainDefinitionContent: JSON.stringify(captainDefinition) } + ) + ); + } + updateConfigAndSave(appName: string, appDefinition: IAppDef) { var instanceCount = appDefinition.instanceCount; var envVars = appDefinition.envVars; diff --git a/app-frontend/src/containers/apps/oneclick/OneClickAppConfigPage.tsx b/app-frontend/src/containers/apps/oneclick/OneClickAppConfigPage.tsx index 27abe07..900574c 100644 --- a/app-frontend/src/containers/apps/oneclick/OneClickAppConfigPage.tsx +++ b/app-frontend/src/containers/apps/oneclick/OneClickAppConfigPage.tsx @@ -22,7 +22,8 @@ export interface IOneClickVariable { } export interface IDockerComposeService { - image: string; + image?: string; + dockerFileLines?: string[]; // This is our property, not DockerCompose. We use this instead of image if we need to extend the image. volumes?: string[]; ports?: string[]; environment?: IHashMapGeneric; diff --git a/app-frontend/src/containers/apps/oneclick/OneClickAppDeployHelper.ts b/app-frontend/src/containers/apps/oneclick/OneClickAppDeployHelper.ts index 3de61b7..fd0126b 100644 --- a/app-frontend/src/containers/apps/oneclick/OneClickAppDeployHelper.ts +++ b/app-frontend/src/containers/apps/oneclick/OneClickAppDeployHelper.ts @@ -5,6 +5,8 @@ import { IDockerComposeService } from "./OneClickAppConfigPage"; import Utils from "../../../utils/Utils"; +import { IAppDef } from "../AppDefinition"; +import OneClickAppDeploymentHelper from "./OneClickAppDeploymentHelper"; const REGISTERING = "REGISTERING"; const CONFIGURING = "CONFIGURING"; @@ -22,14 +24,14 @@ export interface IDeploymentState { } export default class OneClickAppDeployHelper { - private apiManager: ApiManager; + private deploymentHelper: OneClickAppDeploymentHelper = new OneClickAppDeploymentHelper(); private template: IOneClickTemplate | undefined; constructor( private onDeploymentStateChanged: ( deploymentState: IDeploymentState ) => void ) { - this.apiManager = new ApiManager(); + // } startDeployProcess( @@ -201,10 +203,31 @@ export default class OneClickAppDeployHelper { dockerComposeService: IDockerComposeService ): IDeploymentStep[] { const promises: IDeploymentStep[] = []; + const self = this; - // promise to create - // promise to update - // promise to deploy + promises.push({ + stepName: `Registering ${appName}`, + stepPromise: self.deploymentHelper.createRegisterPromise( + appName, + dockerComposeService + ) + }); + + promises.push({ + stepName: `Configuring ${appName} (volumes, ports, environmental variables)`, + stepPromise: self.deploymentHelper.createConfigurationPromise( + appName, + dockerComposeService + ) + }); + + promises.push({ + stepName: `Deploying ${appName} (might take up to a minute)`, + stepPromise: self.deploymentHelper.createDeploymentPromise( + appName, + dockerComposeService + ) + }); return promises; } diff --git a/app-frontend/src/containers/apps/oneclick/OneClickAppDeploymentHelper.ts b/app-frontend/src/containers/apps/oneclick/OneClickAppDeploymentHelper.ts new file mode 100644 index 0000000..625d7bb --- /dev/null +++ b/app-frontend/src/containers/apps/oneclick/OneClickAppDeploymentHelper.ts @@ -0,0 +1,111 @@ +import { IDockerComposeService } from "./OneClickAppConfigPage"; +import ApiManager from "../../../api/ApiManager"; +import { IAppDef } from "../AppDefinition"; +import { ICaptainDefinition } from "../../../models/ICaptainDefinition"; +import Utils from "../../../utils/Utils"; + +export default class OneClickAppDeploymentHelper { + private apiManager: ApiManager = new ApiManager(); + + createRegisterPromise( + appName: string, + dockerComposeService: IDockerComposeService + ) { + const self = this; + return Promise.resolve().then(function() { + return self.apiManager.registerNewApp( + appName, + !!dockerComposeService.volumes && !!dockerComposeService.volumes.length + ); + }); + } + + createConfigurationPromise( + appName: string, + dockerComposeService: IDockerComposeService + ) { + const self = this; + return Promise.resolve().then(function() { + return self.apiManager + .getAllApps() + .then(function(data) { + const appDefs = data.appDefinitions as IAppDef[]; + for (let index = 0; index < appDefs.length; index++) { + const element = appDefs[index]; + if (element.appName === appName) { + return Utils.copyObject(element); + } + } + }) + .then(function(appDef) { + if (!appDef) { + throw new Error("App was not found right after registering!!"); + } + + appDef.volumes = appDef.volumes || []; + + const vols = dockerComposeService.volumes || []; + for (let i = 0; i < vols.length; i++) { + const elements = vols[i].split(":"); + if (elements[0].startsWith("/")) { + appDef.volumes.push({ + hostPath: elements[0], + containerPath: elements[1] + }); + } else { + appDef.volumes.push({ + volumeName: elements[0], + containerPath: elements[1] + }); + } + } + + appDef.ports = appDef.ports || []; + const ports = dockerComposeService.ports || []; + for (let i = 0; i < ports.length; i++) { + const elements = ports[i].split(":"); + appDef.ports.push({ + hostPort: Number(elements[0]), + containerPort: Number(elements[1]) + }); + } + + appDef.envVars = appDef.envVars || []; + const environment = dockerComposeService.environment || {}; + Object.keys(environment).forEach(function(envKey) { + appDef.envVars.push({ + key: envKey, + value: environment[envKey] + }); + }); + + return self.apiManager.updateConfigAndSave(appName, appDef); + }); + }); + } + + createDeploymentPromise( + appName: string, + dockerComposeService: IDockerComposeService + ) { + const self = this; + return Promise.resolve().then(function() { + let captainDefinition: ICaptainDefinition = { + schemaVersion: 2 + }; + + if (dockerComposeService.image) { + captainDefinition.imageName = dockerComposeService.image; + } else { + captainDefinition.dockerfileLines = + dockerComposeService.dockerFileLines; + } + + return self.apiManager.uploadCaptainDefinitionContent( + appName, + captainDefinition, + false + ); + }); + } +} diff --git a/app-frontend/src/models/ICaptainDefinition.ts b/app-frontend/src/models/ICaptainDefinition.ts new file mode 100644 index 0000000..d11eda8 --- /dev/null +++ b/app-frontend/src/models/ICaptainDefinition.ts @@ -0,0 +1,6 @@ +export interface ICaptainDefinition { + schemaVersion: number + dockerfileLines?: string[] + imageName?: string + templateId?: string +}