diff --git a/src/app/backend/apihandler.go b/src/app/backend/apihandler.go index b65d559d59fc291e3fc74aca63f34c108b1b7a36..3f0f4109c1813a6f1f2ed4f916d5e49aa4d4d6a2 100644 --- a/src/app/backend/apihandler.go +++ b/src/app/backend/apihandler.go @@ -33,8 +33,8 @@ func CreateHttpApiHandler(client *client.Client) http.Handler { deployWs.Route( deployWs.POST(""). To(apiHandler.handleDeploy). - Reads(DeployAppConfig{}). - Writes(DeployAppConfig{})) + Reads(AppDeployment{}). + Writes(AppDeployment{})) wsContainer.Add(deployWs) microserviceListWs := new(restful.WebService) @@ -55,7 +55,7 @@ type ApiHandler struct { // Handles deploy API call. func (apiHandler *ApiHandler) handleDeploy(request *restful.Request, response *restful.Response) { - cfg := new(DeployAppConfig) + cfg := new(AppDeployment) if err := request.ReadEntity(cfg); err != nil { handleInternalError(response, err) return diff --git a/src/app/backend/deploy.go b/src/app/backend/deploy.go index abf3a369e8f3ec77b7367fcd323ad5d4d169b678..19c73d48a1dccd76fcdfff7140d30c8e705cf205 100644 --- a/src/app/backend/deploy.go +++ b/src/app/backend/deploy.go @@ -15,43 +15,113 @@ package main import ( - "math/rand" - "strconv" - "time" - api "k8s.io/kubernetes/pkg/api" client "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/util" ) // Configuration for an app deployment. -type DeployAppConfig struct { +type AppDeployment struct { // Name of the application. - AppName string `json:"appName"` + Name string `json:"name"` // Docker image path for the application. ContainerImage string `json:"containerImage"` + + // Number of replicas of the image to maintain. + Replicas int `json:"replicas"` + + // Port mappings for the service that is created. The service is created if there is at least + // one port mapping. + PortMappings []PortMapping `json:"portMappings"` + + // Whether the created service is external. + IsExternal bool `json:"isExternal"` } -// Deploys an app based on the given configuration. The app is deployed using the given client. -func DeployApp(config *DeployAppConfig, client *client.Client) error { - // TODO(bryk): The implementation below is just for tests. To complete an end-to-end setup of - // the project. It'll be replaced with a real implementation. - rand.Seed(time.Now().UTC().UnixNano()) - podName := config.AppName + "-" + strconv.Itoa(rand.Intn(10000)) +// Port mapping for an application deployment. +type PortMapping struct { + // Port that will be exposed on the service. + Port int `json:"port"` + + // Docker image path for the application. + TargetPort int `json:"targetPort"` + + // IP protocol for the mapping, e.g., "TCP" or "UDP". + Protocol api.Protocol `json:"protocol"` +} - pod := api.Pod{ +// Deploys an app based on the given configuration. The app is deployed using the given client. +// App deployment consists of a replication controller and an optional service. Both of them share +// common labels. +// TODO(bryk): Write tests for this function. +func DeployApp(deployment *AppDeployment, client *client.Client) error { + podTemplate := &api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ - Name: podName, + Labels: map[string]string{"name": deployment.Name}, }, Spec: api.PodSpec{ Containers: []api.Container{{ - Name: config.AppName, - Image: config.ContainerImage, + Name: deployment.Name, + Image: deployment.ContainerImage, }}, }, } - _, err := client.Pods(api.NamespaceDefault).Create(&pod) + replicaSet := &api.ReplicationController{ + ObjectMeta: api.ObjectMeta{ + Name: deployment.Name, + }, + Spec: api.ReplicationControllerSpec{ + Replicas: deployment.Replicas, + Selector: map[string]string{"name": deployment.Name}, + Template: podTemplate, + }, + } + + _, err := client.ReplicationControllers(api.NamespaceDefault).Create(replicaSet) + + if err != nil { + // TODO(bryk): Roll back created resources in case of error. + return err + } + + if len(deployment.PortMappings) > 0 { + service := &api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: deployment.Name, + Labels: map[string]string{"name": deployment.Name}, + }, + Spec: api.ServiceSpec{ + Selector: map[string]string{"name": deployment.Name}, + }, + } - return err + if deployment.IsExternal { + service.Spec.Type = api.ServiceTypeLoadBalancer + } else { + service.Spec.Type = api.ServiceTypeNodePort + } + + for _, portMapping := range deployment.PortMappings { + servicePort := + api.ServicePort{ + Protocol: portMapping.Protocol, + Port: portMapping.Port, + TargetPort: util.IntOrString{ + Kind: util.IntstrInt, + IntVal: portMapping.TargetPort, + }, + } + service.Spec.Ports = append(service.Spec.Ports, servicePort) + } + + _, err = client.Services(api.NamespaceDefault).Create(service) + + // TODO(bryk): Roll back created resources in case of error. + + return err + } else { + return nil + } } diff --git a/src/app/externs/backendapi.js b/src/app/externs/backendapi.js index bafa527feab05706e4ef10950cc2b1f23e263cef..3747151c36133114ec0db24e4532b4444e024c5a 100644 --- a/src/app/externs/backendapi.js +++ b/src/app/externs/backendapi.js @@ -22,12 +22,27 @@ * @externs */ + const backendApi = {}; + +/** + * @typedef {{ + * port: (number|null), + * protocol: string, + * targetPort: (number|null) + * }} + */ +backendApi.PortMapping; + + /** * @typedef {{ - * appName: string, - * containerImage: string + * containerImage: string, + * isExternal: boolean, + * name: string, + * portMappings: !Array, + * replicas: number * }} */ -backendApi.DeployAppConfig; +backendApi.AppDeployment; diff --git a/src/app/frontend/deploy/deploy.controller.js b/src/app/frontend/deploy/deploy.controller.js index 56538a2ee264e5e0e38de0fe15bf49ec189c25cb..1d044fbb6aa81890b3d41e3866238ae46fcddc3d 100644 --- a/src/app/frontend/deploy/deploy.controller.js +++ b/src/app/frontend/deploy/deploy.controller.js @@ -22,20 +22,43 @@ export default class DeployController { /** * @param {!angular.$resource} $resource * @param {!angular.$log} $log + * @param {!ui.router.$state} $state * @ngInject */ - constructor($resource, $log) { + constructor($resource, $log, $state) { /** @export {string} */ - this.appName = ''; + this.name = ''; /** @export {string} */ this.containerImage = ''; - /** @private {!angular.Resource} */ + /** @export {number} */ + this.replicas = 1; + + /** + * List of supported protocols. + * TODO(bryk): Do not hardcode the here, move to backend. + * @const @export {!Array} + */ + this.protocols = ['TCP', 'UDP']; + + /** @export {!Array} */ + this.portMappings = [this.newEmptyPortMapping_(this.protocols[0])]; + + /** @export {boolean} */ + this.isExternal = false; + + /** @private {!angular.Resource} */ this.resource_ = $resource('/api/deploy'); /** @private {!angular.$log} */ this.log_ = $log; + + /** @private {!ui.router.$state} */ + this.state_ = $state; + + /** @private {boolean} */ + this.isDeployInProgress_ = false; } /** @@ -44,19 +67,68 @@ export default class DeployController { * @export */ deploy() { - /** @type {!backendApi.DeployAppConfig} */ + // TODO(bryk): Validate input data before sending to the server. + + /** @type {!backendApi.AppDeployment} */ let deployAppConfig = { - appName: this.appName, containerImage: this.containerImage, + isExternal: this.isExternal, + name: this.name, + portMappings: this.portMappings.filter(this.isPortMappingEmpty_), + replicas: this.replicas, }; + this.isDeployInProgress_ = true; this.resource_.save( deployAppConfig, (savedConfig) => { - this.log_.info('Succesfully deployed application: ', savedConfig); + this.isDeployInProgress_ = false; + this.log_.info('Successfully deployed application: ', savedConfig); + this.state_.go('servicelist'); }, (err) => { - this.log_.error('Error deployng application:', err); + this.isDeployInProgress_ = false; + this.log_.error('Error deploying application:', err); }); } + + /** + * Returns true when the deploy action should be enabled. + * @return {boolean} + * @export + */ + isDeployDisabled() { + return this.isDeployInProgress_; + } + + /** + * Cancels the deployment form. + * @export + */ + cancel() { + this.state_.go('zero'); + } + + /** + * @param {string} defaultProtocol + * @return {!backendApi.PortMapping} + * @private + */ + newEmptyPortMapping_(defaultProtocol) { + return { + port: null, + targetPort: null, + protocol: defaultProtocol, + }; + } + + /** + * Returns true when the given port mapping hasn't been filled by the user, i.e., is empty. + * @param {!backendApi.PortMapping} portMapping + * @return {boolean} + * @private + */ + isPortMappingEmpty_(portMapping) { + return !!portMapping.port && !!portMapping.targetPort; + } } diff --git a/src/app/frontend/deploy/deploy.html b/src/app/frontend/deploy/deploy.html index d03709e2fa3ea891065af2b818490f8156770964..a56b991ed04bd719180d02e8aa864d35131161d7 100644 --- a/src/app/frontend/deploy/deploy.html +++ b/src/app/frontend/deploy/deploy.html @@ -17,17 +17,52 @@ limitations under the License.

Deploy a Containerized App

-
+ - + + + + Specify app details below + + + Upload a YAML or JSON file + + - - + + - Submit - Cancel + + + + +
+ + + + + + + + + + + + + {{protocol}} + + + +
+ + Expose service externally + + + Deploy + + Cancel