提交 f353a432 编写于 作者: P Piotr Bryk

Merge pull request #61 from kubernetes/deploy-app-form

Initial e2e implementation of the deploy view
...@@ -33,8 +33,8 @@ func CreateHttpApiHandler(client *client.Client) http.Handler { ...@@ -33,8 +33,8 @@ func CreateHttpApiHandler(client *client.Client) http.Handler {
deployWs.Route( deployWs.Route(
deployWs.POST(""). deployWs.POST("").
To(apiHandler.handleDeploy). To(apiHandler.handleDeploy).
Reads(DeployAppConfig{}). Reads(AppDeployment{}).
Writes(DeployAppConfig{})) Writes(AppDeployment{}))
wsContainer.Add(deployWs) wsContainer.Add(deployWs)
microserviceListWs := new(restful.WebService) microserviceListWs := new(restful.WebService)
...@@ -55,7 +55,7 @@ type ApiHandler struct { ...@@ -55,7 +55,7 @@ type ApiHandler struct {
// Handles deploy API call. // Handles deploy API call.
func (apiHandler *ApiHandler) handleDeploy(request *restful.Request, response *restful.Response) { func (apiHandler *ApiHandler) handleDeploy(request *restful.Request, response *restful.Response) {
cfg := new(DeployAppConfig) cfg := new(AppDeployment)
if err := request.ReadEntity(cfg); err != nil { if err := request.ReadEntity(cfg); err != nil {
handleInternalError(response, err) handleInternalError(response, err)
return return
......
...@@ -15,43 +15,113 @@ ...@@ -15,43 +15,113 @@
package main package main
import ( import (
"math/rand"
"strconv"
"time"
api "k8s.io/kubernetes/pkg/api" api "k8s.io/kubernetes/pkg/api"
client "k8s.io/kubernetes/pkg/client/unversioned" client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/util"
) )
// Configuration for an app deployment. // Configuration for an app deployment.
type DeployAppConfig struct { type AppDeployment struct {
// Name of the application. // Name of the application.
AppName string `json:"appName"` Name string `json:"name"`
// Docker image path for the application. // Docker image path for the application.
ContainerImage string `json:"containerImage"` 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. // Port mapping for an application deployment.
func DeployApp(config *DeployAppConfig, client *client.Client) error { type PortMapping struct {
// TODO(bryk): The implementation below is just for tests. To complete an end-to-end setup of // Port that will be exposed on the service.
// the project. It'll be replaced with a real implementation. Port int `json:"port"`
rand.Seed(time.Now().UTC().UnixNano())
podName := config.AppName + "-" + strconv.Itoa(rand.Intn(10000)) // 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{ ObjectMeta: api.ObjectMeta{
Name: podName, Labels: map[string]string{"name": deployment.Name},
}, },
Spec: api.PodSpec{ Spec: api.PodSpec{
Containers: []api.Container{{ Containers: []api.Container{{
Name: config.AppName, Name: deployment.Name,
Image: config.ContainerImage, 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
}
} }
...@@ -22,12 +22,27 @@ ...@@ -22,12 +22,27 @@
* @externs * @externs
*/ */
const backendApi = {}; const backendApi = {};
/**
* @typedef {{
* port: (number|null),
* protocol: string,
* targetPort: (number|null)
* }}
*/
backendApi.PortMapping;
/** /**
* @typedef {{ * @typedef {{
* appName: string, * containerImage: string,
* containerImage: string * isExternal: boolean,
* name: string,
* portMappings: !Array<!backendApi.PortMapping>,
* replicas: number
* }} * }}
*/ */
backendApi.DeployAppConfig; backendApi.AppDeployment;
...@@ -22,20 +22,43 @@ export default class DeployController { ...@@ -22,20 +22,43 @@ export default class DeployController {
/** /**
* @param {!angular.$resource} $resource * @param {!angular.$resource} $resource
* @param {!angular.$log} $log * @param {!angular.$log} $log
* @param {!ui.router.$state} $state
* @ngInject * @ngInject
*/ */
constructor($resource, $log) { constructor($resource, $log, $state) {
/** @export {string} */ /** @export {string} */
this.appName = ''; this.name = '';
/** @export {string} */ /** @export {string} */
this.containerImage = ''; this.containerImage = '';
/** @private {!angular.Resource<!backendApi.DeployAppConfig>} */ /** @export {number} */
this.replicas = 1;
/**
* List of supported protocols.
* TODO(bryk): Do not hardcode the here, move to backend.
* @const @export {!Array<string>}
*/
this.protocols = ['TCP', 'UDP'];
/** @export {!Array<!backendApi.PortMapping>} */
this.portMappings = [this.newEmptyPortMapping_(this.protocols[0])];
/** @export {boolean} */
this.isExternal = false;
/** @private {!angular.Resource<!backendApi.AppDeployment>} */
this.resource_ = $resource('/api/deploy'); this.resource_ = $resource('/api/deploy');
/** @private {!angular.$log} */ /** @private {!angular.$log} */
this.log_ = $log; this.log_ = $log;
/** @private {!ui.router.$state} */
this.state_ = $state;
/** @private {boolean} */
this.isDeployInProgress_ = false;
} }
/** /**
...@@ -44,19 +67,68 @@ export default class DeployController { ...@@ -44,19 +67,68 @@ export default class DeployController {
* @export * @export
*/ */
deploy() { deploy() {
/** @type {!backendApi.DeployAppConfig} */ // TODO(bryk): Validate input data before sending to the server.
/** @type {!backendApi.AppDeployment} */
let deployAppConfig = { let deployAppConfig = {
appName: this.appName,
containerImage: this.containerImage, containerImage: this.containerImage,
isExternal: this.isExternal,
name: this.name,
portMappings: this.portMappings.filter(this.isPortMappingEmpty_),
replicas: this.replicas,
}; };
this.isDeployInProgress_ = true;
this.resource_.save( this.resource_.save(
deployAppConfig, deployAppConfig,
(savedConfig) => { (savedConfig) => {
this.log_.info('Succesfully deployed application: ', savedConfig); this.isDeployInProgress_ = false;
this.log_.info('Successfully deployed application: ', savedConfig);
this.state_.go('servicelist');
}, },
(err) => { (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;
}
} }
...@@ -17,17 +17,52 @@ limitations under the License. ...@@ -17,17 +17,52 @@ limitations under the License.
<div layout="column" layout-padding layout-align="center center"> <div layout="column" layout-padding layout-align="center center">
<md-whiteframe class="kd-deploy-whiteframe md-whiteframe-5dp" flex flex-gt-md> <md-whiteframe class="kd-deploy-whiteframe md-whiteframe-5dp" flex flex-gt-md>
<h3 class="md-headline">Deploy a Containerized App</h3> <h3 class="md-headline">Deploy a Containerized App</h3>
<form name="userForm"> <form ng-submit="ctrl.deploy()">
<md-input-container class="md-block"> <md-input-container class="md-block">
<label>App name</label> <label>App name</label>
<input ng-model="ctrl.appName"> <input ng-model="ctrl.name" required>
</md-input-container> </md-input-container>
<md-radio-group ng-model="data.group1">
<md-radio-button class="md-primary">
Specify app details below
</md-radio-button>
<md-radio-button value="bar" class="md-primary">
Upload a YAML or JSON file
</md-radio-button>
</md-radio-group>
<md-input-container class="md-block"> <md-input-container class="md-block">
<label>Container Image</label> <label>Container image</label>
<input ng-model="ctrl.containerImage"> <input ng-model="ctrl.containerImage" required>
</md-input-container> </md-input-container>
<md-button href class="md-raised md-primary" ng-click="ctrl.deploy()">Submit</md-button> <md-input-container class="md-block">
<md-button class="md-raised" ui-sref="zero">Cancel</md-button> <label>Number of pods</label>
<input ng-model="ctrl.replicas" type="number" required min="1">
</md-input-container>
<div layout="row" ng-repeat="portMapping in ctrl.portMappings">
<md-input-container class="md-block">
<label>Port</label>
<input ng-model="portMapping.port" type="number" min="0">
</md-input-container>
<md-input-container class="md-block">
<label>Target port</label>
<input ng-model="portMapping.targetPort" type="number" min="0">
</md-input-container>
<md-input-container class="md-block">
<label>Protocol</label>
<md-select ng-model="portMapping.protocol" required>
<md-option ng-repeat="protocol in ctrl.protocols" ng-value="protocol">
{{protocol}}
</md-option>
</md-select>
</md-input-container>
</div>
<md-switch ng-model="ctrl.isExternal" class="md-primary">
Expose service externally
</md-switch>
<md-button class="md-raised md-primary" type="submit" ng-disabled="ctrl.isDeployDisabled()">
Deploy
</md-button>
<md-button class="md-raised" ng-click="ctrl.cancel()">Cancel</md-button>
</form> </form>
</md-whiteframe> </md-whiteframe>
</div> </div>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册