提交 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 {
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
......
......@@ -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
}
}
......@@ -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<!backendApi.PortMapping>,
* replicas: number
* }}
*/
backendApi.DeployAppConfig;
backendApi.AppDeployment;
......@@ -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<!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');
/** @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;
}
}
......@@ -17,17 +17,52 @@ limitations under the License.
<div layout="column" layout-padding layout-align="center center">
<md-whiteframe class="kd-deploy-whiteframe md-whiteframe-5dp" flex flex-gt-md>
<h3 class="md-headline">Deploy a Containerized App</h3>
<form name="userForm">
<form ng-submit="ctrl.deploy()">
<md-input-container class="md-block">
<label>App name</label>
<input ng-model="ctrl.appName">
<input ng-model="ctrl.name" required>
</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">
<label>Container Image</label>
<input ng-model="ctrl.containerImage">
<label>Container image</label>
<input ng-model="ctrl.containerImage" required>
</md-input-container>
<md-button href class="md-raised md-primary" ng-click="ctrl.deploy()">Submit</md-button>
<md-button class="md-raised" ui-sref="zero">Cancel</md-button>
<md-input-container class="md-block">
<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>
</md-whiteframe>
</div>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册