提交 11f1257e 编写于 作者: S Sebastian Florek

Added labels handling in the deploy form.

上级 bf61ab47
......@@ -22,7 +22,6 @@ import (
const (
DescriptionAnnotationKey = "description"
NameLabelKey = "name"
)
// Specification for an app deployment.
......@@ -48,6 +47,9 @@ type AppDeploymentSpec struct {
// Target namespace of the application.
Namespace string `json:"namespace"`
// Labels that will be defined on Pods/RCs/Services
Labels []Label `json:"labels"`
}
// Port mapping for an application deployment.
......@@ -62,13 +64,22 @@ type PortMapping struct {
Protocol api.Protocol `json:"protocol"`
}
// Structure representing label assignable to Pod/RC/Service
type Label struct {
// Label key
Key string `json:"key"`
// Label value
Value string `json:"value"`
}
// 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.Client) error {
annotations := map[string]string{DescriptionAnnotationKey: spec.Description}
labels := map[string]string{NameLabelKey: spec.Name}
labels := getLabelsMap(spec.Labels)
objectMeta := api.ObjectMeta{
Annotations: annotations,
Name: spec.Name,
......@@ -137,3 +148,14 @@ func DeployApp(spec *AppDeploymentSpec, client *client.Client) error {
return nil
}
}
// Converts array of labels to map[string]string
func getLabelsMap(labels []Label) map[string]string {
result := make(map[string]string)
for _, label := range labels {
result[label.Key] = label.Value
}
return result
}
......@@ -41,7 +41,8 @@ backendApi.PortMapping;
* description: string,
* portMappings: !Array<!backendApi.PortMapping>,
* replicas: number,
* namespace: string
* namespace: string,
* labels: !Array<!backendApi.Label>
* }}
*/
backendApi.AppDeploymentSpec;
......@@ -106,3 +107,11 @@ backendApi.NamespaceSpec;
* }}
*/
backendApi.NamespaceList;
/**
* @typedef {{
* key: string,
* value: string
* }}
*/
backendApi.Label;
......@@ -10,5 +10,8 @@
"angular/module-getter": 0,
// Disable undefined variable checking. This is done by the compiler.
"no-undef": 0,
// To allow comparison to undefined variable, because Google Closure Compiler does not
// understand angular.isDefined correctly.
"angular/definedundefined": 0,
}
}
......@@ -17,7 +17,7 @@ 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 ng-submit="ctrl.deployBySelection()">
<form name="ctrl.deployForm" ng-submit="ctrl.deployBySelection()">
<md-input-container class="md-block">
<label>App name</label>
<input ng-model="ctrl.name" required>
......
......@@ -30,6 +30,9 @@ export default class DeployController {
* @ngInject
*/
constructor($resource, $log, $state, $mdDialog, namespaces) {
/** @export {!angular.FormController} Initialized from the template */
this.deployForm;
/** @export {string} */
this.name = '';
......@@ -108,7 +111,7 @@ export default class DeployController {
* @return {boolean}
* @export
*/
isDeployDisabled() { return this.isDeployInProgress_; }
isDeployDisabled() { return this.isDeployInProgress_ || this.deployForm.$invalid; }
/**
* Cancels the deployment form.
......
......@@ -14,6 +14,7 @@
import stateConfig from './deploy_state';
import deployFromSettingsDirective from './deployfromsettings_directive';
import deployLabelDirective from './deploylabel_directive';
/**
* Angular module for the deploy view.
......@@ -24,7 +25,9 @@ export default angular.module(
'kubernetesDashboard.deploy',
[
'ngMaterial',
'ngResource',
'ui.router',
])
.config(stateConfig)
.directive('deployFromSettings', deployFromSettingsDirective);
.directive('deployFromSettings', deployFromSettingsDirective)
.directive('kdLabel', deployLabelDirective);
......@@ -47,3 +47,15 @@ limitations under the License.
<label>Description (optional)</label>
<textarea ng-model="ctrl.description"></textarea>
</md-input-container>
<div>
<div>Labels (optional)</div>
<div layout="column">
<div layout="row">
<p flex>Key</p>
<p flex>Value</p>
</div>
<div ng-repeat="label in ctrl.labels">
<kd-label layout="row" flex label="label" labels="ctrl.labels"></kd-label>
</div>
</div>
</div>
......@@ -12,8 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import DeployLabel from './deploylabel';
import {stateName as replicasetliststate} from 'replicasetlist/replicasetlist_state';
// Label keys for predefined labels
export const APP_LABEL_KEY = 'app';
export const VERSION_LABEL_KEY = 'version';
/**
* Controller for the deploy from settings directive.
*
......@@ -68,6 +73,13 @@ export default class DeployFromSettingsController {
/** @export {boolean} */
this.isExternal = false;
/** @export {!Array<!DeployLabel>} */
this.labels = [
new DeployLabel(APP_LABEL_KEY, '', false, this.getName_.bind(this)),
new DeployLabel(VERSION_LABEL_KEY, '', false, this.getContainerImageVersion_.bind(this)),
new DeployLabel(),
];
}
/**
......@@ -87,6 +99,7 @@ export default class DeployFromSettingsController {
portMappings: this.portMappings.filter(this.isPortMappingEmpty_),
replicas: this.replicas,
namespace: this.namespace,
labels: this.toBackendApiLabels_(this.labels),
};
let defer = this.q_.defer();
......@@ -107,6 +120,22 @@ export default class DeployFromSettingsController {
return defer.promise;
}
/**
* Converts array of DeployLabel to array of backend api label
* @param {!Array<!DeployLabel>} labels
* @return {!Array<!backendApi.Label>}
* @private
*/
toBackendApiLabels_(labels) {
// Omit labels with empty key/value
/** @type {!Array<!DeployLabel>} */
let apiLabels =
labels.filter((label) => { return label.key.length !== 0 && label.value().length !== 0; });
// Transform to array of backend api labels
return apiLabels.map((label) => { return label.toBackendApi(); });
}
/**
* @param {string} defaultProtocol
* @return {!backendApi.PortMapping}
......@@ -123,4 +152,31 @@ export default class DeployFromSettingsController {
* @private
*/
isPortMappingEmpty_(portMapping) { return !!portMapping.port && !!portMapping.targetPort; }
/**
* Callbacks used in DeployLabel model to make it aware of controller state changes.
*/
/**
* Returns extracted from link container image version.
* @return {string}
* @private
*/
getContainerImageVersion_() {
/** @type {number} */
let index = (this.containerImage || '').lastIndexOf(':');
if (index > -1) {
return this.containerImage.substring(index + 1);
}
return '';
}
/**
* Returns application name.
* @return {string}
* @private
*/
getName_() { return this.name; }
}
<!--
Copyright 2015 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-form layout="row" flex name="labelForm">
<md-input-container md-no-float class="md-block" flex="45">
<input name="key" ng-model="labelCtrl.label.key" ng-change="labelCtrl.check(labelForm)"
placeholder="{{labelCtrl.label.key}}" ng-disabled="!labelCtrl.label.editable">
<ng-messages for="labelForm.key.$error" ng-if="labelForm.key.$invalid">
<ng-message when="unique">{{labelCtrl.label.key}} is not unique.</ng-message>
</ng-messages>
</md-input-container>
<p flex="5"></p>
<md-input-container md-no-float class="md-block" flex="40">
<input ng-model="labelCtrl.label.value" ng-change="labelCtrl.check()"
placeholder="{{labelCtrl.label.value}}" ng-disabled="!labelCtrl.label.editable"
ng-model-options="{ getterSetter: true }">
<ng-messages ng-if="labelForm.key.$error.unique"></ng-messages>
</md-input-container>
<md-button type="button" ng-show="labelCtrl.isRemovable()"
ng-click="labelCtrl.deleteLabel()"
class="material-icons kd-deploy-labels-button">delete
</md-button>
</ng-form>
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* Represents label object used in deploy form view.
* @final
*/
export default class DeployLabel {
/**
* Constructs DeployLabel object.
* @param {string} key
* @param {string} value
* @param {boolean} editable
* @param {function(string): string} derivedValueGetterFn - callback
*/
constructor(key = '', value = '', editable = true, derivedValueGetterFn = undefined) {
/** @export {boolean} */
this.editable = editable;
/** @export {string} */
this.key = key;
/** @private {string} */
this.value_ = value;
/** @private {function(string): string|undefined} */
this.derivedValueGetter_ = derivedValueGetterFn;
}
/**
* @param {string} [newValue]
* @return {string}
* @export
*/
value(newValue) {
if (this.derivedValueGetter_ !== undefined) {
if (newValue !== undefined) {
throw Error("Can not set value of derived label.");
}
return this.derivedValueGetter_(this.key);
}
return newValue !== undefined ? (this.value_ = newValue) : this.value_;
}
/**
* Converts 'this' object to backendApi.Label object.
* @return {!backendApi.Label}
* @export
*/
toBackendApi() {
return {
key: this.key,
value: this.value(),
};
}
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
.kd-deploy-labels-button {
min-width: 10px;
margin: 0;
padding: 0;
height: 10px;
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import DeployLabel from './deploylabel';
/**
* Service used for handling label actions like: hover, showing duplicated key error, etc.
* @final
*/
export default class DeployLabelController {
/**
* Constructs our label controller.
*/
constructor() {
/** @export {!DeployLabel} Initialized from the scope. */
this.label;
/** @export {!Array<!DeployLabel>} Initialized from the scope. */
this.labels;
}
/**
* Calls checks on label:
* - adds label if last empty label has been filled
* - removes label if some label in the middle has key and value empty
* - checks for duplicated key and sets validity of element
* @param {!angular.FormController|undefined} labelForm
* @export
*/
check(labelForm) {
this.addIfNeeded_();
this.removeIfNeeded_();
this.validateKey_(labelForm);
}
/**
* Returns true when label is editable and is not last on the list.
* Used to indicate whether delete icon should be shown near label.
* @return {boolean}
* @export
*/
isRemovable() {
/** @type {!DeployLabel} */
let lastElement = this.labels[this.labels.length - 1];
return !!(this.label.editable && this.label !== lastElement);
}
/**
* Deletes row from labels list.
* @export
*/
deleteLabel() {
/** @type {number} */
let rowIdx = this.labels.indexOf(this.label);
if (rowIdx > -1) {
this.labels.splice(rowIdx, 1);
}
}
/**
* Adds label if last label key and value has been filled.
* @private
*/
addIfNeeded_() {
/** @type {!DeployLabel} */
let lastLabel = this.labels[this.labels.length - 1];
if (this.isFilled_(lastLabel)) {
this.addNewLabel_();
}
}
/**
* Adds row to labels list.
* @private
*/
addNewLabel_() { this.labels.push(new DeployLabel()); }
/**
* Removes label from labels list if label is empty and is not last label.
* @private
*/
removeIfNeeded_() {
/** @type {!DeployLabel} */
let lastLabel = this.labels[this.labels.length - 1];
if (this.isEmpty_(this.label) && this.label !== lastLabel) {
this.deleteLabel();
}
}
/**
* Validates label withing label form.
* Current checks:
* - duplicated key
* @param {!angular.FormController|undefined} labelForm
* @private
*/
validateKey_(labelForm) {
if (angular.isDefined(labelForm)) {
/** @type {!angular.NgModelController} */
let elem = labelForm.key;
// TODO(floreks): Validate label key/value.
/** @type {boolean} */
let isValid = !this.isDuplicated_();
elem.$setValidity('unique', isValid);
}
}
/**
* Returns true if there are 2 or more labels with the same key on the labelList,
* false otherwise.
* @return {boolean}
* @private
*/
isDuplicated_() {
/** @type {number} */
let duplications = 0;
this.labels.forEach((label) => {
if (this.label.key.length !== 0 && label.key === this.label.key) {
duplications++;
}
});
return duplications > 1;
}
/**
* Returns true if label key and value are empty, false otherwise.
* @param {!DeployLabel} label
* @return {boolean}
* @private
*/
isEmpty_(label) { return label.key.length === 0 && label.value().length === 0; }
/**
* Returns true if label key and value are not empty, false otherwise.
* @param {!DeployLabel} label
* @return {boolean}
* @private
*/
isFilled_(label) { return label.key.length !== 0 && label.value().length !== 0; }
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import DeployLabelController from './deploylabel_controller';
/**
* Returns directive definition for deploy form label.
*
* @return {!angular.Directive}
*/
export default function labelDirective() {
return {
controller: DeployLabelController,
controllerAs: 'labelCtrl',
templateUrl: 'deploy/deploylabel.html',
scope: {},
bindToController: {
label: '=',
labels: '=',
},
};
}
......@@ -4,7 +4,6 @@
},
// Define missing global Angular testing environment variables.
"globals": {
"inject": true,
"module": true,
"angular": true,
},
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import DeployFromSettingController from 'deploy/deployfromsettings_controller';
import deployModule from 'deploy/deploy_module';
describe('DeployFromSettings controller', () => {
let ctrl;
beforeEach(() => {
angular.mock.module(deployModule.name);
angular.mock.inject(($log, $state, $resource, $q) => {
ctrl = new DeployFromSettingController($log, $state, $resource, $q);
});
});
it('should return empty string when containerImage is undefined', () => {
ctrl.containerImage = undefined;
expect(ctrl.getContainerImageVersion_()).toEqual('');
});
it('should return empty string when containerImage is empty', () => {
ctrl.containerImage = '';
expect(ctrl.getContainerImageVersion_()).toEqual('');
});
it('should return empty string when containerImage is not empty and does not contain `:`' +
' delimiter',
() => {
ctrl.containerImage = 'test';
expect(ctrl.getContainerImageVersion_()).toEqual('');
});
it('should return part of the string after `:` delimiter', () => {
ctrl.containerImage = 'test:1';
expect(ctrl.getContainerImageVersion_()).toEqual('1');
});
});
......@@ -15,12 +15,12 @@
import ZerostateController from 'zerostate/zerostate_controller';
describe('Main controller', () => {
let vm;
let ctrl;
beforeEach(inject(($timeout) => { vm = new ZerostateController($timeout); }));
beforeEach(angular.mock.inject(() => { ctrl = new ZerostateController(); }));
it('should do something', () => {
expect(vm.learnMoreLinks).toEqual([
expect(ctrl.learnMoreLinks).toEqual([
{title: 'Dashboard Tour', link: "#"},
{title: 'Deploying your App', link: "#"},
{title: 'Monitoring your App', link: "#"},
......
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
describe('Deploy view', function() {
beforeEach(function() { browser.get('#/deploy'); });
it('should not contain errors in console', function() {
browser.manage().logs().get('browser').then(function(browserLog) {
// Filter and search for errors logs
let filteredLogs = browserLog.filter((log) => { return log.level.value > 900; });
// Expect no error logs
expect(filteredLogs.length).toBe(0);
});
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册