提交 9a24f04d 编写于 作者: B bryk

Resource requirements and deploy form validation

上级 fa76e6dc
......@@ -17,7 +17,8 @@ package main
import (
"log"
api "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/resource"
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/util/intstr"
)
......@@ -57,6 +58,12 @@ type AppDeploymentSpec struct {
// Target namespace of the application.
Namespace string `json:"namespace"`
// Optional memory requirement for the container.
MemoryRequirement *resource.Quantity `json:"memoryRequirement"`
// Optional CPU requirement for the container.
CpuRequirement *resource.Quantity `json:"cpuRequirement"`
// Labels that will be defined on Pods/RCs/Services
Labels []Label `json:"labels"`
......@@ -88,7 +95,6 @@ type Label struct {
// 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(spec *AppDeploymentSpec, client client.Interface) error {
log.Printf("Deploying %s application into %s namespace", spec.Name, spec.Namespace)
......@@ -109,16 +115,25 @@ func DeployApp(spec *AppDeploymentSpec, client client.Interface) error {
SecurityContext: &api.SecurityContext{
Privileged: &spec.RunAsPrivileged,
},
Resources: api.ResourceRequirements{
Requests: make(map[api.ResourceName]resource.Quantity),
},
}
if spec.ContainerCommand != nil {
containerSpec.Command = []string{*spec.ContainerCommand}
}
if spec.ContainerCommandArgs != nil {
containerSpec.Args = []string{*spec.ContainerCommandArgs}
}
if spec.CpuRequirement != nil {
containerSpec.Resources.Requests[api.ResourceCPU] = *spec.CpuRequirement
}
if spec.MemoryRequirement != nil {
containerSpec.Resources.Requests[api.ResourceMemory] = *spec.MemoryRequirement
}
podTemplate := &api.PodTemplateSpec{
ObjectMeta: objectMeta,
Spec: api.PodSpec{
......
......@@ -53,6 +53,8 @@ backendApi.Label;
* labels: !Array<!backendApi.Label>,
* replicas: number,
* namespace: string,
* memoryRequirement: ?string,
* cpuRequirement: ?number,
* runAsPrivileged: boolean,
* }}
*/
......
......@@ -17,7 +17,8 @@ limitations under the License.
<kd-help-section>
<md-input-container class="md-block" md-is-error="ctrl.isNameError()">
<label>App name</label>
<input ng-model="ctrl.name" name="name" namespace="ctrl.namespace" required kd-unique-name ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }">
<input ng-model="ctrl.name" name="name" namespace="ctrl.namespace" required kd-unique-name
ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }">
<md-progress-linear class="kd-deploy-form-name-progress" md-mode="indeterminate"
ng-show="ctrl.form.name.$pending">
</md-progress-linear>
......@@ -42,7 +43,8 @@ limitations under the License.
</ng-messages>
</md-input-container>
<kd-user-help>
Enter the URL of a public image on any registry, or a private image hosted on Docker Hub or Google Container Registry.
Enter the URL of a public image on any registry, or a private image hosted on Docker Hub or
Google Container Registry.
<a href="">Learn more</a>
</kd-user-help>
</kd-help-section>
......@@ -50,7 +52,11 @@ limitations under the License.
<kd-help-section>
<md-input-container class="md-block">
<label>Number of pods</label>
<input ng-model="ctrl.replicas" type="number" required min="1">
<input ng-model="ctrl.replicas" type="number" required min="1" name="replicas">
<ng-messages for="ctrl.form.replicas.$error" role="alert" multiple>
<ng-message when="number">Number of pods must be a positive integer.</ng-message>
<ng-message when="min">Number of pods must be positive.</ng-message>
</ng-messages>
</md-input-container>
<kd-user-help>
A Replica Set will be created to maintain the desired number of pods across your cluster.
......@@ -59,27 +65,40 @@ limitations under the License.
</kd-help-section>
<kd-help-section>
<div layout="row" ng-repeat="portMapping in ctrl.portMappings">
<md-input-container flex>
<label>Port</label>
<input ng-model="portMapping.port" type="number" min="0">
</md-input-container>
<md-input-container flex>
<label>Target port</label>
<input ng-model="portMapping.targetPort" type="number" min="0">
</md-input-container>
<md-input-container flex="none">
<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 ng-repeat="portMapping in ctrl.portMappings">
<ng-form name="portMappingForm" layout="row">
<md-input-container flex>
<label>Port</label>
<input ng-model="portMapping.port" type="number" min="0" name="port">
<ng-messages for="portMappingForm.port.$error" role="alert" multiple>
<ng-message when="number">Port must be an integer.</ng-message>
<ng-message when="min">Port must be non-negative.</ng-message>
</ng-messages>
</md-input-container>
<md-input-container flex>
<label>Target port</label>
<input ng-model="portMapping.targetPort" type="number" min="0" name="targetPort">
<ng-messages for="portMappingForm.targetPort.$error" role="alert" multiple>
<ng-message when="number">Target port must be an integer.</ng-message>
<ng-message when="min">Target port must be non-negative.</ng-message>
</ng-messages>
</md-input-container>
<md-input-container flex="none">
<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>
</ng-form>
</div>
<kd-user-help>
Ports are optional. If specified, a Service will be created mapping the Port (incoming) to a target Port seen by the container.
<span ng-if="ctrl.name">The internal DNS name for this Service will be: <b>{{ctrl.name}}</b>.</span>
Ports are optional. If specified, a Service will be created mapping the Port (incoming) to a
target Port seen by the container.
<span ng-if="ctrl.name">
The internal DNS name for this Service will be: <b>{{ctrl.name}}</b>.
</span>
<a href="">Learn more</a>
</kd-user-help>
</kd-help-section>
......@@ -93,17 +112,18 @@ limitations under the License.
<div ng-show="ctrl.isMoreOptionsEnabled()">
<kd-help-section>
<md-input-container>
<label>Description (optional)</label>
<label>Description</label>
<textarea ng-model="ctrl.description"></textarea>
</md-input-container>
<kd-user-help>
The description will be added as an annotation to the Replica Set and displayed in the application's details.
The description will be added as an annotation to the Replica Set and displayed in the
application's details.
</kd-user-help>
</kd-help-section>
<kd-help-section>
<div>
<div>Labels (optional)</div>
<div>Labels</div>
<div layout="column">
<div layout="row">
<p flex>Key</p>
......@@ -115,7 +135,8 @@ limitations under the License.
</div>
</div>
<kd-user-help>
The specified labels will be applied to the created Replica Set, Service (if any) and Pods. Common labels include release, environment, tier, partition and track.
The specified labels will be applied to the created Replica Set, Service (if any) and Pods.
Common labels include release, environment, tier, partition and track.
<a href="">Learn more</a>
</kd-user-help>
</kd-help-section>
......@@ -138,15 +159,40 @@ limitations under the License.
</kd-user-help>
</kd-help-section>
<kd-help-section>
<div layout="row">
<md-input-container class="md-block">
<label>CPU requirement (cores)</label>
<input ng-model="ctrl.cpuRequirement" type="number" name="cpuRequirement" min="0">
<ng-messages for="ctrl.form.cpuRequirement.$error" role="alert" multiple>
<ng-message when="number">CPU requirement must be a valid number.</ng-message>
<ng-message when="min">CPU requirement must be positive.</ng-message>
</ng-messages>
</md-input-container>
<md-input-container class="md-block">
<label>Memory requirement (MiB)</label>
<input ng-model="ctrl.memoryRequirement" type="number" name="memoryRequirement" min="0">
<ng-messages for="ctrl.form.memoryRequirement.$error" role="alert" multiple>
<ng-message when="number">Memory requirement must be a valid number.</ng-message>
<ng-message when="min">Memory requirement must be positive.</ng-message>
</ng-messages>
</md-input-container>
</div>
<kd-user-help>
You can specify minimum CPU and memory requirements for the container.
<a href="">Learn more</a>
</kd-user-help>
</kd-help-section>
<kd-help-section>
<div>
<md-input-container class="md-block">
<label>Run command (optional)</label>
<label>Run command</label>
<input ng-model="ctrl.containerCommand">
</md-input-container>
<md-input-container class="md-block">
<label>Run command arguments (optional)</label>
<label>Run command arguments</label>
<input ng-model="ctrl.containerCommandArgs">
</md-input-container>
</div>
......
......@@ -112,6 +112,16 @@ export default class DeployFromSettingsController {
*/
this.namespace = this.namespaces[0];
/**
* @export {?number}
*/
this.cpuRequirement = null;
/**
* @type {?number}
*/
this.memoryRequirement = null;
/** @private {!angular.$q} */
this.q_ = $q;
......@@ -147,6 +157,9 @@ export default class DeployFromSettingsController {
portMappings: this.portMappings.filter(this.isPortMappingEmpty_),
replicas: this.replicas,
namespace: this.namespace,
cpuRequirement: angular.isNumber(this.cpuRequirement) ? this.cpuRequirement : null,
memoryRequirement: angular.isNumber(this.memoryRequirement) ? `${this.memoryRequirement}Mi` :
null,
labels: this.toBackendApiLabels_(this.labels),
runAsPrivileged: this.runAsPrivileged,
};
......
......@@ -19,6 +19,7 @@ import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/client/unversioned/testclient"
)
......@@ -48,6 +49,9 @@ func TestDeployApp(t *testing.T) {
SecurityContext: &api.SecurityContext{
Privileged: &spec.RunAsPrivileged,
},
Resources: api.ResourceRequirements{
Requests: make(map[api.ResourceName]resource.Quantity),
},
}},
},
},
......@@ -102,3 +106,32 @@ func TestDeployAppContainerCommands(t *testing.T) {
commandArgs, container.Args)
}
}
func TestDeployWithResourceRequirements(t *testing.T) {
cpuRequirement := resource.Quantity{}
memoryRequirement := resource.Quantity{}
spec := &AppDeploymentSpec{
Namespace: "foo-namespace",
Name: "foo-name",
CpuRequirement: &cpuRequirement,
MemoryRequirement: &memoryRequirement,
}
expectedResources := api.ResourceRequirements{
Requests: map[api.ResourceName]resource.Quantity{
api.ResourceMemory: memoryRequirement,
api.ResourceCPU: cpuRequirement,
},
}
testClient := testclient.NewSimpleFake()
DeployApp(spec, testClient)
createAction := testClient.Actions()[0].(testclient.CreateActionImpl)
rc := createAction.GetObject().(*api.ReplicationController)
container := rc.Spec.Template.Spec.Containers[0]
if !reflect.DeepEqual(container.Resources, expectedResources) {
t.Errorf("Expected resource requirements to be %#v but got %#v",
expectedResources, container.Resources)
}
}
......@@ -172,6 +172,48 @@ describe('DeployFromSettings controller', () => {
expect(resourceObject.save).toHaveBeenCalled();
});
it('should deploy with resource requirements', () => {
// given
let resourceObject = {
save: jasmine.createSpy('save'),
};
mockResource.and.returnValue(resourceObject);
resourceObject.save.and.callFake(function(spec) {
// then
expect(spec.cpuRequirement).toBe(77);
expect(spec.memoryRequirement).toBe('88Mi');
});
ctrl.cpuRequirement = 77;
ctrl.memoryRequirement = 88;
// when
ctrl.deploy();
// then
expect(resourceObject.save).toHaveBeenCalled();
});
it('should deploy with empty resource requirements', () => {
// given
let resourceObject = {
save: jasmine.createSpy('save'),
};
mockResource.and.returnValue(resourceObject);
resourceObject.save.and.callFake(function(spec) {
// then
expect(spec.cpuRequirement).toBe(null);
expect(spec.memoryRequirement).toBe(null);
});
ctrl.cpuRequirement = null;
ctrl.memoryRequirement = '';
// when
ctrl.deploy();
// then
expect(resourceObject.save).toHaveBeenCalled();
});
it('should hide more options by default', () => {
// this is default behavior so no given/when
// then
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册