Cluster options

https://github.com/rancher/rancher/issues/13076
This commit is contained in:
loganhz 2018-06-08 00:37:14 +08:00
parent cd5cdecb2c
commit c8d5b3b640
8 changed files with 340 additions and 284 deletions

View File

@ -5,8 +5,11 @@ import { get, set, computed, observer } from '@ember/object';
import { satisfies } from 'shared/utils/parse-version';
import { sortVersions } from 'shared/utils/sort';
import { inject as service } from '@ember/service';
import { validateEndpoint } from 'shared/utils/util';
import { camelToUnderline, underlineToCamel, removeEmpty, keysToCamel, validateEndpoint } from 'shared/utils/util';
import { isSafari } from 'ui/utils/platform';
import C from 'shared/utils/constants';
import YAML from 'npm:yamljs';
import json2yaml from 'npm:json2yaml';
import layout from './template';
import { resolve } from 'rsvp';
@ -42,7 +45,9 @@ export default Component.extend(ClusterDriver, {
clusterState: alias('model.originalCluster.state'),
configField: 'rancherKubernetesEngineConfig',
registry: 'default',
accept: '.yml, .yaml',
loading: false,
pasteOrUpload: false,
model: null,
initialVersion: null,
registryUrl: null,
@ -81,6 +86,9 @@ export default Component.extend(ClusterDriver, {
},
didInsertElement() {
set(this, '_boundChange', (event) => { this.change(event); });
this.$('INPUT[type=file]').on('change', get(this, '_boundChange'));
if ( ! get(this,'cluster.rancherKubernetesEngineConfig') ) {
const globalStore = get(this, 'globalStore');
let config = globalStore.createRecord({
@ -140,10 +148,46 @@ export default Component.extend(ClusterDriver, {
}
},
change(event) {
var input = event.target;
if ( input.files && input.files[0] ) {
let file = input.files[0];
var reader = new FileReader();
reader.onload = (event2) => {
var out = event2.target.result;
set(this, 'pastedConfig', out);
input.value = '';
};
reader.readAsText(file);
}
},
actualAccept: computed('accept', function() {
if ( isSafari ) {
return '';
} else {
return get(this, 'accept');
}
}),
actions: {
setNodePoolErrors(errors) {
set(this, 'nodePoolErrors', errors);
},
cancel() {
set(this, 'pasteOrUpload', false);
},
showPaste() {
set(this, 'pasteOrUpload', true);
},
upload() {
this.$('INPUT[type=file]')[0].click();
},
},
willSave() {
@ -315,4 +359,129 @@ export default Component.extend(ClusterDriver, {
return out;
}),
getResourceFields: function(type) {
const schema = get(this, 'globalStore').getById('schema', type.toLowerCase());
return schema ? get(schema, 'resourceFields') : null;
},
getFieldValue: function(field, type) {
if ( type.startsWith('map[') ) {
type = type.slice(4, type.length -1);
const resourceFields = this.getResourceFields(type);
if ( resourceFields ) {
if ( field ) {
const out = {};
Object.keys(field).forEach((key) => {
out[camelToUnderline(key)] = this.getFieldValue(field[key], type);
});
return out;
} else {
return null;
}
} else {
if ( field ) {
const out = {};
Object.keys(field).forEach((key) => {
out[camelToUnderline(key)] = field[key];
});
return out;
} else {
return null;
}
}
} else if ( type.startsWith('array[') ) {
type = type.slice(6, type.length -1);
const resourceFields = this.getResourceFields(type);
if ( resourceFields ) {
return field ? field.map((item) => {
return this.getFieldValue(item, type);
}) : null;
} else {
return field ? field.map((item) => {
return item;
}) : null;
}
} else {
const resourceFields = this.getResourceFields(type);
if ( resourceFields ) {
const out = {};
Object.keys(resourceFields).forEach((key) => {
if ( field !== undefined && field !== null && (typeof field !== 'object' || Object.keys(field).length ) ) {
out[camelToUnderline(key)] = this.getFieldValue(field[key], resourceFields[key].type);
}
});
return out;
} else {
return field;
}
}
},
getSupportedFields: function(source, tragetField) {
const out = {};
const resourceFields = this.getResourceFields(tragetField);
Object.keys(resourceFields).forEach((key) => {
const field = get(source, key);
const type = resourceFields[key].type;
const value = this.getFieldValue(field, type);
out[camelToUnderline(key)] = value;
});
return out;
},
pastedConfig: computed('pasteOrUpload', {
get() {
const intl = get(this, 'intl');
let config = this.getSupportedFields(get(this, 'cluster.rancherKubernetesEngineConfig'), 'rancherKubernetesEngineConfig');
if ( !config ) {
return '';
}
config = removeEmpty(config);
while ( JSON.stringify(config) !== JSON.stringify(removeEmpty(config)) ){
config = removeEmpty(config);
}
let yaml = json2yaml.stringify(config);
const lines = yaml.split('\n');
lines.shift();
let out = '';
lines.forEach((line) => {
if ( line.trim() ) {
const key = `rkeConfigComment.${line.split(':')[0].trim()}`
if ( intl.exists(key) ) {
const commentLines = intl.t(key).split('\n');
commentLines.forEach((commentLine) => {
out += `# ${commentLine.slice(1, commentLine.length - 1)}\n`;
});
}
out += `${line.slice(2)}\n`;
}
});
return out;
},
set(key, value) {
let configs;
try {
configs = YAML.parse(value);
} catch ( err ) {
set(this, 'clusterOptErrors', [`Cluster Options Parse Error: ${err.snippet} - ${err.message}`]);
return value;
}
set(this, 'clusterOptErrors', []);
const validFields = this.getResourceFields('rancherKubernetesEngineConfig');
Object.keys(configs || {}).forEach((key) => {
if ( validFields[underlineToCamel(key)] ) {
set(this, `cluster.rancherKubernetesEngineConfig.${underlineToCamel(key)}`, keysToCamel(configs[key]));
}
});
return value;
}
}),
});

View File

@ -7,88 +7,114 @@
expand=(action expandFn)
}}
<div class="row">
<div class="col span-4">
<label class="acc-label">{{t 'clusterNew.rke.version.label'}}</label>
{{new-select
content=versionChoices
optionLabelPath='value'
value=config.kubernetesVersion
}}
</div>
<div class="col span-4">
<label class="acc-label">{{t 'clusterNew.rke.network.label'}}</label>
{{new-select
classNames="form-control"
content=networkChoices
localizedLabel=true
value=config.network.plugin
}}
</div>
<div class="col span-4">
<label class="acc-label">{{t 'clusterNew.rke.podSecurityPolicy.label'}}</label>
<div class="radio">
<label>
{{radio-button selection=config.services.kubeApi.podSecurityPolicy value=false disabled=(not model.psps.length)}}
{{t 'generic.disabled'}}
</label>
</div>
<div class="radio">
<label class={{unless model.psps.length 'text-muted'}}>
{{radio-button selection=config.services.kubeApi.podSecurityPolicy value=true disabled=(not model.psps.length)}}
{{t 'generic.enabled'}}
{{#unless model.psps.length}}
&mdash; {{t 'clusterNew.psp.none'}}
{{/unless}}
</label>
</div>
</div>
</div>
<div class="row">
<div class="col span-6">
<label class="acc-label">{{t 'clusterNew.rke.ignoreDockerVersion.label'}}</label>
<div class="radio">
<label>
{{radio-button selection=config.ignoreDockerVersion value=false}}
{{t 'clusterNew.rke.ignoreDockerVersion.disabled'}}
</label>
</div>
<div class="radio">
<label>
{{radio-button selection=config.ignoreDockerVersion value=true}}
{{t 'clusterNew.rke.ignoreDockerVersion.enabled'}}
</label>
</div>
</div>
<div class="col span-6">
{{#if config.services.kubeApi.podSecurityPolicy}}
<label class="acc-label">{{t 'clusterNew.psp.label'}}{{field-required}}</label>
{{new-select
content=model.psps
optionLabelPath='displayName'
optionValuePath='id'
prompt='clusterNew.psp.prompt'
localizedPrompt=true
value=cluster.defaultPodSecurityPolicyTemplateId
disabled=(not config.services.kubeApi.podSecurityPolicy)
}}
<div class="btn-group pull-right">
{{#if pasteOrUpload}}
<button class="btn btn-sm" {{action 'cancel'}}>{{t 'clusterNew.advanced.cancel'}}</button>
{{else}}
<label class="acc-label">{{t 'clusterNew.psp.label'}}</label>
<div class="form-control-static">{{t 'generic.none'}}</div>
<button class="btn btn-sm bg-primary" {{action 'showPaste'}}>{{t 'clusterNew.advanced.yaml'}} <span class="icon icon-copy"></span></button>
{{/if}}
<button class="btn btn-sm bg-primary" {{action 'upload'}}>{{t 'uploadFile.label'}} <span class="icon icon-upload"></span></button>
</div>
</div>
<div class="row">
<div class="col span-12">
{{cru-cloud-provider
cluster=model.cluster
driver=nodeWhich
{{#if pasteOrUpload}}
<div class="banner bg-info">
<div class="banner-icon p-10"><i class="icon icon-info"></i></div>
<div class="banner-message">
{{t 'clusterNew.advanced.helpText'}}
</div>
</div>
<div class="mt-25">
{{input-yaml
showUpload=false
showDownload=false
canChangeName=false
value=pastedConfig
}}
</div>
</div>
{{#advanced-section advanced=advanced}}
{{cluster-options-rke errors=clusterOptErrors config=cluster.rancherKubernetesEngineConfig advanced=advanced}}
{{/advanced-section}}
{{copy-to-clipboard tooltipText="" buttonText="copyToClipboard.tooltip" clipboardText=pastedConfig class="with-clip"}}
{{else}}
<div class="row">
<div class="col span-4">
<label class="acc-label">{{t 'clusterNew.rke.version.label'}}</label>
{{new-select
content=versionChoices
optionLabelPath='value'
value=config.kubernetesVersion
}}
</div>
<div class="col span-4">
<label class="acc-label">{{t 'clusterNew.rke.network.label'}}</label>
{{new-select
classNames="form-control"
content=networkChoices
localizedLabel=true
value=config.network.plugin
}}
</div>
<div class="col span-4">
<label class="acc-label">{{t 'clusterNew.rke.podSecurityPolicy.label'}}</label>
<div class="radio">
<label>
{{radio-button selection=config.services.kubeApi.podSecurityPolicy value=false disabled=(not model.psps.length)}}
{{t 'generic.disabled'}}
</label>
</div>
<div class="radio">
<label class={{unless model.psps.length 'text-muted'}}>
{{radio-button selection=config.services.kubeApi.podSecurityPolicy value=true disabled=(not model.psps.length)}}
{{t 'generic.enabled'}}
{{#unless model.psps.length}}
&mdash; {{t 'clusterNew.psp.none'}}
{{/unless}}
</label>
</div>
</div>
</div>
<div class="row">
<div class="col span-6">
<label class="acc-label">{{t 'clusterNew.rke.ignoreDockerVersion.label'}}</label>
<div class="radio">
<label>
{{radio-button selection=config.ignoreDockerVersion value=false}}
{{t 'clusterNew.rke.ignoreDockerVersion.disabled'}}
</label>
</div>
<div class="radio">
<label>
{{radio-button selection=config.ignoreDockerVersion value=true}}
{{t 'clusterNew.rke.ignoreDockerVersion.enabled'}}
</label>
</div>
</div>
<div class="col span-6">
{{#if config.services.kubeApi.podSecurityPolicy}}
<label class="acc-label">{{t 'clusterNew.psp.label'}}{{field-required}}</label>
{{new-select
content=model.psps
optionLabelPath='displayName'
optionValuePath='id'
prompt='clusterNew.psp.prompt'
localizedPrompt=true
value=cluster.defaultPodSecurityPolicyTemplateId
disabled=(not config.services.kubeApi.podSecurityPolicy)
}}
{{else}}
<label class="acc-label">{{t 'clusterNew.psp.label'}}</label>
<div class="form-control-static">{{t 'generic.none'}}</div>
{{/if}}
</div>
</div>
<div class="row">
<div class="col span-12">
{{cru-cloud-provider
cluster=model.cluster
driver=nodeWhich
}}
</div>
</div>
{{/if}}
{{/accordion-list-item}}
{{/accordion-list}}
@ -207,3 +233,4 @@
<button {{action "close"}} class="btn bg-primary">{{t 'clusterNew.rke.done'}}</button>
</div>
{{/if}}
<input type="file" accept="{{actualAccept}}" class="hide">

View File

@ -1,174 +0,0 @@
import Component from '@ember/component';
import layout from './template';
import { get, set, computed } from '@ember/object';
import { inject as service } from '@ember/service';
import YAML from 'npm:yamljs';
import json2yaml from 'npm:json2yaml';
function removeEmpty(obj){
return Object.keys(obj)
.filter(k => obj[k] !== null && obj[k] !== undefined && (typeof obj[k] !== 'object' || (Object.keys(obj[k]).filter((key) => key !== 'type').length)))
.reduce((newObj, k) =>
typeof obj[k] === 'object' ?
Object.assign(newObj, {[k]: removeEmpty(obj[k])}) :
Object.assign(newObj, {[k]: obj[k]}),
{});
}
const BLACK_LIST_FIELD = ['kubernetesVersion', 'cloudProvider', 'ignoreDockerVersion'];
export default Component.extend({
layout,
globalStore: service(),
intl: service(),
config: null,
advanced: null,
errors: null,
originConfigKeys: null,
getResourceFields: function(type) {
const schema = get(this, 'globalStore').getById('schema', type.toLowerCase());
return schema ? get(schema, 'resourceFields') : null;
},
getFieldValue: function(field, type) {
if ( type.startsWith('map[') ) {
type = type.slice(4, type.length -1);
const resourceFields = this.getResourceFields(type);
if ( resourceFields ) {
if ( field ) {
const out = {};
Object.keys(field).forEach((key) => {
out[key] = this.getFieldValue(field[key], type);
});
return out;
} else {
return null;
}
} else {
if ( field ) {
const out = {};
Object.keys(field).forEach((key) => {
out[key] = field[key];
});
return out;
} else {
return null;
}
}
} else if ( type.startsWith('array[') ) {
type = type.slice(6, type.length -1);
const resourceFields = this.getResourceFields(type);
if ( resourceFields ) {
return field ? field.map((item) => {
return this.getFieldValue(item, type);
}) : null;
} else {
return field ? field.map((item) => {
return item;
}) : null;
}
} else {
const resourceFields = this.getResourceFields(type);
if ( resourceFields ) {
const out = {};
Object.keys(resourceFields).forEach((key) => {
if ( field !== undefined && field !== null && (typeof field !== 'object' || Object.keys(field).length ) ) {
out[key] = this.getFieldValue(field[key], resourceFields[key].type);
}
});
return out;
} else {
return field;
}
}
},
getSupportedFields: function(source, tragetField) {
const out = {};
const resourceFields = this.getResourceFields(tragetField);
Object.keys(resourceFields).forEach((key) => {
if ( BLACK_LIST_FIELD.indexOf(key) > -1 ) {
return;
}
const field = get(source, key);
const type = resourceFields[key].type;
const value = this.getFieldValue(field, type);
out[key] = value;
});
return out;
},
pastedConfig: computed('config', {
get() {
const intl = get(this, 'intl');
let config = this.getSupportedFields(get(this, 'config'), 'rancherKubernetesEngineConfig');
if ( !config ) {
return '';
}
config = removeEmpty(config);
while ( JSON.stringify(config) !== JSON.stringify(removeEmpty(config)) ){
config = removeEmpty(config);
}
if ( !get(this, 'originConfigKeys') ) {
const originConfigKeys = [];
Object.keys(config).forEach((key) => {
originConfigKeys.push(key);
});
set(this, 'originConfigKeys', originConfigKeys);
}
let yaml = json2yaml.stringify(config);
const lines = yaml.split('\n');
lines.shift();
let out = '';
lines.forEach((line) => {
if ( line.trim() ) {
const key = `rkeConfigComment.${line.split(':')[0].trim()}`
if ( intl.exists(key) ) {
const commentLines = intl.t(key).split('\n');
commentLines.forEach((commentLine) => {
out += `# ${commentLine.slice(1, commentLine.length - 1)}\n`;
});
}
out += `${line.slice(2)}\n`;
}
});
return out;
},
set(key, value) {
let configs;
try {
configs = YAML.parse(value);
} catch ( err ) {
set(this, 'errors', [`Cluster advanced options parse error: ${err.snippet} - ${err.message}`]);
return value;
}
set(this, 'errors', []);
const validFields = this.getResourceFields('rancherKubernetesEngineConfig');
Object.keys(configs || {}).forEach((key) => {
if ( validFields[key] ) {
set(this, `config.${key}`, configs[key]);
}
});
const originConfigKeys = get(this, 'originConfigKeys');
if ( originConfigKeys ) {
originConfigKeys.filter((key) => Object.keys(configs || {}).indexOf(key) === -1)
.forEach((key) => {
set(this, `config.${key}`, null);
});
}
return value;
}
}),
});

View File

@ -1,18 +0,0 @@
<hr class="mb-15"/>
<label class="acc-label mt-5">{{t 'clusterNew.advanced.label'}}</label>
<div class="banner bg-info">
<div class="banner-icon p-10"><i class="icon icon-info"></i></div>
<div class="banner-message">
{{t 'clusterNew.advanced.helpText'}}
</div>
</div>
<div class="mt-25">
{{#if advanced}}
{{input-yaml
showUpload=false
showDownload=false
canChangeName=false
value=pastedConfig
}}
{{/if}}
</div>

View File

@ -47,10 +47,10 @@
<div class="row">
<div class="col span-6">
<label class="acc-label pb-0">{{key}}</label>
<p class="help-block mt-0">{{t (get (get azureDescriptions key) "description")}}</p>
<p class="help-block mt-0">{{t (get (get azureDescriptions key) "description")}}</p>
</div>
<div class="col span-6">
{{input class="form-control input-sm value" spellcheck="false" type="text" value=value placeholder=(t 'generic.value')}}
{{input class="form-control input-sm value" spellcheck="false" type="text" value=(mut (get configAnswers key)) placeholder=(t 'generic.value')}}
</div>
</div>
{{/each-in}}

View File

@ -345,6 +345,52 @@ export function isBadTld(name) {
}
}
export function removeEmpty(obj){
return Object.keys(obj)
.filter(k => obj[k] !== null && obj[k] !== undefined && (typeof obj[k] !== 'object' || (Object.keys(obj[k]).filter((key) => key !== 'type').length)))
.reduce((newObj, k) =>
typeof obj[k] === 'object' ?
Object.assign(newObj, {[k]: removeEmpty(obj[k])}) :
Object.assign(newObj, {[k]: obj[k]}),
{});
}
export function camelToUnderline(str) {
str = (str || '');
if ( str.indexOf('-') > -1 ) {
return str;
} else {
return (str || '').dasherize().split('-').join('_');
}
}
export function underlineToCamel(str) {
return (str || '').split('_').map((t, i) => {
if ( i === 0 ) {
return t;
} else {
return t === 'qps' ? 'QPS' : t.capitalize();
}
}).join('');
}
export function keysToCamel(obj) {
if ( !typeof(obj) === "object" || typeof(obj) === "string" || typeof(obj) === "number" || typeof(obj) === "boolean" ) {
return obj;
}
const keys = Object.keys(obj);
let n = keys.length;
while (n--) {
const key = keys[n];
const titleKey = underlineToCamel(key);
obj[titleKey] = keysToCamel(obj[key]);
if ( key !== titleKey ) {
delete obj[key];
}
}
return obj;
}
var Util = {
absoluteUrl: absoluteUrl,
addAuthorization: addAuthorization,
@ -353,6 +399,7 @@ var Util = {
arrayDiff: arrayDiff,
arrayIntersect: arrayIntersect,
camelToTitle: camelToTitle,
camelToUnderline: camelToUnderline,
constructUrl: constructUrl,
download: download,
escapeHtml: escapeHtml,
@ -366,18 +413,21 @@ var Util = {
isBadTld: isBadTld,
isNumeric: isNumeric,
isPrivate: isPrivate,
keysToCamel: keysToCamel,
lcFirst: lcFirst,
parseUrl: parseUrl,
pluralize: pluralize,
popupWindowOptions: popupWindowOptions,
random32: random32,
randomStr: randomStr,
removeEmpty: removeEmpty,
sortableNumericSuffix: sortableNumericSuffix,
strPad: strPad,
stripScheme: stripScheme,
timerFuzz: timerFuzz,
ucFirst: ucFirst,
uniqKeys: uniqKeys,
underlineToCamel: underlineToCamel,
validateEndpoint: validateEndpoint,
};

View File

@ -1837,8 +1837,9 @@ clusterRow:
clusterNew:
advanced:
label: Advanced Options
helpText: Tabs are not able to be used in the yaml file for parsing.
cancel: Edit as a Form
yaml: Edit as YAML
name:
label: Cluster Name
placeholder: e.g. sandbox
@ -5827,30 +5828,30 @@ rkeConfigComment:
""
" network:"
" plugin: calico"
" calicoNetworkProvider:"
" cloudProvider: aws"
" calico_network_provider:"
" cloud_provider: aws"
""
" # To specify flannel interface"
""
" network:"
" plugin: flannel"
" flannelNetworkProvider:"
" flannel_network_provider:"
" iface: eth1"
""
" # To specify flannel interface for canal plugin"
""
" network:"
" plugin: canal"
" canalNetworkProvider:"
" canal_network_provider:"
" iface: eth1"
services: |
""
" services:"
" kubeApi:"
" serviceClusterIpRange: 10.43.0.0/16"
" kubeController:"
" clusterCidr: 10.42.0.0/16"
" serviceClusterIpRange: 10.43.0.0/16"
" kube_api:"
" service_cluster_ip_range: 10.43.0.0/16"
" kube_controller:"
" cluster_cidr: 10.42.0.0/16"
" service_cluster_ip_range: 10.43.0.0/16"
" kubelet:"
" clusterDomain: cluster.local"
" clusterDnsServer: 10.43.0.10"
" cluster_domain: cluster.local"
" cluster_dns_server: 10.43.0.10"

View File

@ -1783,8 +1783,9 @@ clusterWelcome:
<p>为容器提供安全的Overlay网络使容器相互之间可以安全的进行跨主机通信。</p>
clusterNew:
advanced:
label: 高级选项
helpText: 不要使用TabsTabs字符在yaml中不能解析
cancel: 表单输入
yaml: 编辑YAML
name:
label: 集群名称
placeholder: 例如Sandbox