Split listeners and portRules

This commit is contained in:
Vincent Fiduccia 2017-05-22 20:57:47 -07:00
parent e234dfb492
commit 5d3a22e29f
No known key found for this signature in database
GPG Key ID: 2B29AD6BB2BB2582
26 changed files with 621 additions and 430 deletions

View File

@ -15,11 +15,14 @@ export default Ember.Route.extend({
let extInfo = parseExternalId(stack.get('externalId'));
deps.push(catalog.fetchTemplate(extInfo.templateId, false));
});
return Ember.RSVP.all(deps).then((ahray) => {
ahray.forEach((ary) => {
let stck = stacks.findBy('externalIdInfo.templateId', ary.id);
stck.catalogTemplateInfo = ary; // need that generica catalog icon
return Ember.RSVP.all(deps).then((templates) => {
templates.forEach((template) => {
stacks.filterBy('externalIdInfo.templateId', template.id).forEach((stack) => {
Ember.set(stack,'catalogTemplateInfo',template);
});
});
return Ember.Object.create({
stacks: stacks,
});

View File

@ -62,14 +62,6 @@
{{/if}}
{{/sortable-table}}
{{else}}
<div class="row">
<div class="col span-6 offset-3 text-center pt-40 pb-40">
<img style="width: 75%;" src="{{app.baseAssets}}assets/images/resources/container.svg"/>
<a class="btn bg-link icon-btn mt-50" href="{{href-to 'balancers.new'}}">
<span class="darken"><i class="icon icon-plus text-small"></i></span>
<span>{{t 'nav.containers.addBalancer'}}</span>
</a>
</div>
</div>
{{empty-table resource="container" newRoute="balancers.new" newTranslationKey="nav.containers.addBalancer"}}
{{/if}}
</section>

View File

@ -9,7 +9,7 @@ export default Ember.Controller.extend({
actions: {
cancel() {
this.transitionToRoute(this.get('parentRoute'));
this.send('goToPrevious','apps-tab.index');
}
},
});

View File

@ -16,7 +16,6 @@ export default Ember.Component.extend(NewOrEdit, {
isGlobal : null,
isRequestedHost : null,
upgradeOptions : null,
hasUnsupportedPorts : false,
// Errors from components
ruleErrors : null,
@ -30,7 +29,6 @@ export default Ember.Component.extend(NewOrEdit, {
this._super(...arguments);
this.labelsChanged();
this.get('service').initPorts();
this.updatePorts();
},
actions: {
@ -64,119 +62,6 @@ export default Ember.Component.extend(NewOrEdit, {
return this.get('intl').t(k);
}.property('intl.locale','needsUpgrade','isService','isVm','service.secondaryLaunchConfigs.length'),
// ----------------------------------
// Ports
// ----------------------------------
updatePorts() {
let rules = this.get('service.lbConfig.portRules')||[];
let publish = [];
let expose = [];
// Set ports and publish on the launch config
rules.forEach((rule) => {
// The inner one eliminates null/undefined, then the outer one
// converts integers to string (so they can be re-parsed later)
let srcStr = ((rule.get('sourcePort')||'')+'').trim();
let src = parseInt(srcStr,10);
if ( !src || src < 1 || src > 65535 ) {
return;
}
let entry = src+":"+src+"/"+rule.get('ipProtocol');
if ( rule.get('access') === 'public' ) {
// Source IP applies only to public rules
let ip = rule.get('sourceIp');
if ( ip ) {
// IPv6
if ( ip.indexOf(":") >= 0 && ip.substr(0,1) !== '[' ) {
entry = '['+ip+']:' + entry;
} else {
entry = ip + ':' + entry;
}
}
publish.push(entry);
} else {
expose.push(entry);
}
});
this.get('service.launchConfig').setProperties({
ports: publish.uniq(),
expose: expose.uniq(),
});
},
shouldUpdatePorts: function() {
Ember.run.once(this,'updatePorts');
}.observes('service.lbConfig.portRules.@each.{sourceIp,sourcePort,access,protocol}'),
validateRules() {
let intl = this.get('intl');
let rules = this.get('service.lbConfig.portRules');
let errors = [];
let seen = {};
// Set ports and publish on the launch config
// And also do a bunch of validation while we're here
rules.forEach((rule) => {
// The inner one eliminates null/undefined, then the outer one
// converts integers to string (so they can be re-parsed later)
let srcStr = ((rule.get('sourcePort')||'')+'').trim();
let tgtStr = ((rule.get('targetPort')||'')+'').trim();
if ( !srcStr ) {
errors.push(intl.t('newBalancer.error.noSourcePort'));
return;
}
let src = parseInt(srcStr,10);
if ( !src || src < 1 || src > 65535 ) {
errors.push(intl.t('newBalancer.error.invalidSourcePort', {num: srcStr}));
} else if ( !tgtStr ) {
tgtStr = srcStr;
}
let tgt = parseInt(tgtStr,10);
if ( !tgt || tgt < 1 || tgt > 65535 ) {
errors.push(intl.t('newBalancer.error.invalidTargetPort', {num: tgtStr}));
return;
}
let sourceIp = rule.get('sourceIp');
let key;
if ( sourceIp ) {
key = '['+sourceIp+']:' + src;
} else {
key = '[0.0.0.0]:' + src;
}
let access = rule.get('access');
let id = access + '-' + rule.get('protocol') + '-' + src;
if ( seen[key] ) {
if ( seen[key] !== id ) {
errors.push(intl.t('newBalancer.error.mixedPort', {num: src}));
}
} else {
seen[key] = id;
}
if ( !rule.get('serviceId') && !rule.get('selector') ) {
errors.push(intl.t('newBalancer.error.noTarget'));
}
// Make ports always numeric
rule.setProperties({
sourcePort: src,
targetPort: tgt,
});
});
this.set('ruleErrors', errors);
},
needsUpgrade: function() {
function arrayToStr(map) {
map = map || {};
@ -278,8 +163,6 @@ export default Ember.Component.extend(NewOrEdit, {
// Save
// ----------------------------------
willSave() {
this.validateRules();
let ok = this._super(...arguments);
if ( ok && !this.get('isUpgrade') ) {
// Set the stack ID

View File

@ -34,9 +34,11 @@
</div>
<section class="horizontal-form container-fluid">
{{form-balancer-rules
{{form-balancer-listeners
service=service
errors=ruleErrors
}}
<hr/>
</section>
{{container/form-scheduling

View File

@ -0,0 +1,207 @@
import Ember from 'ember';
import { parsePortSpec } from 'ui/utils/parse-port';
export default Ember.Component.extend({
intl: Ember.inject.service(),
service: null,
ports: null,
protocolChoices: null,
showBackend: null,
errors: null,
onInit: function() {
let rules = this.get('service.lbConfig.portRules');
let ports = [];
rules.forEach((rule) => {
let kind = 'service';
if ( !!rule.selector ) {
kind= 'selector';
} else if ( rule.instanceId ) {
rule.kind = 'instance';
}
rule.set('kind', kind);
});
(this.get('service.launchConfig.ports')||[]).forEach((str) => {
let parsed = parsePortSpec(str);
let obj = Ember.Object.create({
access: 'public',
protocol: null,
sourcePort: parsed.hostPort,
sourceIp: parsed.hostIp,
rules: [],
});
ports.push(obj);
});
(this.get('service.launchConfig.expose')||[]).forEach((str) => {
let parsed = parsePortSpec(str);
let obj = Ember.Object.create({
access: 'internal',
protocol: null,
sourcePort: parsed.hostPort,
sourceIp: null,
rules: [],
});
ports.push(obj);
});
// Filter the rules into the right port
ports.forEach((obj) => {
obj.set('rules',rules.filter((x) => {
return x.sourcePort === obj.sourcePort
}));
obj.set('protocol', obj.get('rules.firstObject.protocol')||'http');
});
this.set('ports', ports);
if ( ports.length === 0 ) {
this.send('addPort');
}
let protos = this.get('store').getById('schema','portrule').optionsFor('protocol');
protos.removeObject('udp');
protos.sort();
this.set('protocolChoices', protos);
if ( this.get('showBackend') === null ) {
let hasName = !!rules.findBy('backendName');
this.set('showBackend', hasName);
}
}.on('init'),
shouldFlattenAndValidate: function() {
Ember.run.once(this,'flattenAndValidate');
}.observes('ports.@each.{sourcePort,protocol,access,sourceIp,rules}'),
flattenAndValidate() {
let intl = this.get('intl');
let ports = this.get('ports');
let errors = [];
let rules = [];
let publish = [];
let expose = [];
let seen = {};
// Set ports and publish on the launch config
// And also do a bunch of validation while we're here
ports.forEach((port) => {
// 1. Set expose/ports and ensure valid ports/protocols
let srcStr = ((port.get('sourcePort')||'')+'').trim();
if ( !srcStr ) {
errors.push(intl.t('newBalancer.error.noSourcePort'));
return;
}
let src = parseInt(srcStr,10);
if ( !src || src < 1 || src > 65535 ) {
errors.push(intl.t('newBalancer.error.invalidSourcePort', {num: srcStr}));
}
let sourceIp = port.get('sourceIp');
let uniqueKey;
if ( sourceIp ) {
uniqueKey = '['+sourceIp+']:' + src;
} else {
uniqueKey = '[0.0.0.0]:' + src;
}
let access = port.get('access');
let protocol = port.get('protocol');
let id = access + '-' + protocol + '-' + src;
if ( seen[uniqueKey] ) {
if ( seen[uniqueKey] !== id ) {
errors.push(intl.t('newBalancer.error.mixedPort', {num: src}));
return;
}
} else {
seen[uniqueKey] = id;
}
let entry = src+":"+src+"/"+ ( protocol === 'udp' ? 'udp' : 'tcp');
if ( access === 'public' ) {
// Source IP applies only to public rules
if ( sourceIp ) {
// IPv6
if ( sourceIp.indexOf(":") >= 0 && sourceIp.substr(0,1) !== '[' ) {
entry = '['+sourceIp+']:' + entry;
} else {
entry = sourceIp + ':' + entry;
}
}
publish.push(entry);
} else {
expose.push(entry);
}
// 2. Set rules
port.get('rules').forEach((rule) => {
// The inner one eliminates null/undefined, then the outer one
// converts integers to string (so they can be re-parsed later)
let tgtStr = ((rule.get('targetPort')||'')+'').trim();
if ( !tgtStr ) {
errors.push(intl.t('newBalancer.error.noTargetPort'));
return;
}
let tgt = parseInt(tgtStr,10);
if ( !tgt || tgt < 1 || tgt > 65535 ) {
errors.push(intl.t('newBalancer.error.invalidTargetPort', {num: tgtStr}));
return;
}
if ( !rule.get('serviceId') && !rule.get('instanceId') && !rule.get('selector') ) {
errors.push(intl.t('newBalancer.error.noTarget'));
}
// Make ports always numeric
rule.setProperties({
protocol: protocol,
sourcePort: src,
targetPort: tgt,
});
rules.push(rule);
});
});
this.setProperties({
'service.launchConfig.ports': publish.uniq(),
'service.launchConfig.expose': expose.uniq(),
'errors': errors.uniq(),
'service.lbConfig.portRules': rules.sortBy('priority')
});
},
actions: {
addPort() {
let port = Ember.Object.create({
access: 'public',
protocol: 'http',
port: null,
sourceIp: null,
rules: [],
});
this.get('ports').pushObject(port);
},
removePort(port) {
this.get('ports').removeObject(port);
},
showBackend() {
this.set('showBackend', true);
},
rulesChanged() {
this.shouldFlattenAndValidate();
},
},
});

View File

@ -0,0 +1,73 @@
<div>
<label>{{t 'formBalancerListeners.label'}}</label>
<button class="btn bg-link icon-btn ml-20" {{action "addPort"}}>
<span class="darken"><i class="icon icon-plus text-small"></i></span>
<span>{{t 'formBalancerListeners.addPortLabel'}}</span>
</button>
{{#unless showBackend}}
<div class="pull-right clearfix">
<button class="btn bg-transparent p-0 text-small text-right" {{action "showBackend"}}>
{{t 'formBalancerListeners.showBackendLabel'}}
</button>
</div>
{{/unless}}
<hr/>
<p class="text-info">
{{t 'formBalancerListeners.help'}}
</p>
</div>
{{#each ports as |port|}}
<div class="box mb-10">
<div class="row">
<div class="col span-3">
<label class="acc-label">{{t 'formBalancerListeners.sourcePort.label'}}{{field-required}}</label>
{{input-integer class="form-control input-sm" min="1" max="65535" value=port.sourcePort placeholder=(t 'formBalancerListeners.sourcePort.placeholder')}}
</div>
<div class="col span-3">
<label class="acc-label">{{t 'formBalancerListeners.protocol.label'}}</label>
<select class="form-control input-sm" onchange={{action (mut port.protocol) value="target.value"}}>
{{#each protocolChoices as |proto|}}
<option value={{proto}} selected={{eq port.protocol proto}}>{{upper-case proto}}</option>
{{/each}}
</select>
</div>
<div class="col span-3">
<label class="acc-label">{{t 'formBalancerListeners.access.label'}}</label>
<select class="form-control input-sm" onchange={{action (mut port.access) value="target.value"}}>
<option value="public" selected={{eq port.access "public"}}>{{t 'formBalancerListeners.access.public'}}</option>
<option value="internal" selected={{eq port.access "internal"}}>{{t 'formBalancerListeners.access.internal'}}</option>
</select>
</div>
<div class="col span-3">
<label class="acc-label">{{t 'formBalancerListeners.sourceIp.label'}}</label>
{{#if (eq port.access "public")}}
{{input type="text" class="form-control input-sm" value=port.sourceIp placeholder=(t 'formBalancerListeners.sourceIp.placeholder')}}
{{else}}
<div class="text-muted form-control-static">{{t 'generic.na'}}</div>
{{/if}}
</div>
</div>
<hr/>
{{form-balancer-rules
rules=port.rules
protocol=port.protocol
rulesChanged=(action 'rulesChanged')
singleTarget=false
showBackend=showBackend
editing=true
}}
<div class="clearfix">
<button class="btn bg-transparent p-0 text-small pull-right" {{action "removePort" port}}>
{{t 'formBalancerListeners.removePortLabel'}}
</button>
</div>
</div>
{{else}}
<div class="p-20">{{t 'formBalancerListeners.noRules'}}</div>
{{/each}}

View File

@ -1,56 +1,19 @@
import Ember from 'ember';
import { parsePortSpec } from 'ui/utils/parse-port';
export default Ember.Component.extend({
intl: Ember.inject.service(),
service: null,
ruleType: 'portRule',
showListeners: Ember.computed.equal('ruleType','portRule'),
rules: null,
protocolChoices: null,
showBackend: null,
showIp: null,
singleTarget: true,
protocol: null,
editing: true,
onInit: function() {
let rules = this.get('service.lbConfig.portRules');
if ( !rules ) {
rules = [];
this.set('service.lbConfig.portRules', rules);
}
ruleType: 'portRule',
rules.forEach((rule) => {
rule.isSelector = !!rule.selector;
});
this.set('rules', rules);
if ( rules.length === 0 ) {
this.send('addRule');
}
let protos = this.get('store').getById('schema','portrule').optionsFor('protocol');
protos.removeObject('udp');
protos.sort();
this.set('protocolChoices', protos);
if ( this.get('showBackend') === null ) {
let hasName = !!rules.findBy('backendName');
this.set('showBackend', hasName);
}
if ( this.get('showIp') === null ) {
this.get('service.launchConfig.ports').forEach((port) => {
let parsed = parsePortSpec(port,'tcp');
if ( parsed.hostIp ) {
this.set('showIp', true);
}
});
}
}.on('init'),
rulesChanged: function() {
this.sendAction('rulesChanged');
}.observes('rules.@each.{hostname,path,kind,instanceId,serviceId,selector,targetPort,backendName}'),
actions: {
addRule(isSelector) {
addRule(kind) {
let max = 0;
let rules = this.get('rules');
rules.forEach((rule) => {
@ -59,9 +22,8 @@ export default Ember.Component.extend({
rules.pushObject(this.get('store').createRecord({
type: this.get('ruleType'),
access: 'public',
isSelector: isSelector,
protocol: 'http',
kind: kind,
protocol: this.get('protocol'),
priority: max+1,
}));
},
@ -93,16 +55,15 @@ export default Ember.Component.extend({
removeRule(rule) {
this.get('rules').removeObject(rule);
},
showBackend() {
this.set('showBackend', true);
},
showIp() {
this.set('showIp', true);
},
},
protocolChanged: function() {
let protocol = this.get('protocol');
this.get('rules').forEach((rule) => {
rule.set('protocol', protocol);
});
}.observes('protocol'),
updatePriorities() {
let pri = 1;
this.get('rules').forEach((rule) => {
@ -110,27 +71,4 @@ export default Ember.Component.extend({
pri++;
});
},
minPriority: function() {
let val = null;
this.get('rules').forEach((rule) => {
let cur = rule.get('priority');
if ( val === null ) {
val = cur;
} else {
val = Math.min(val, cur);
}
});
return val;
}.property('rules.@each.priority'),
maxPriority: function() {
let val = 0;
this.get('rules').forEach((rule) => {
val = Math.max(val, rule.get('priority'));
});
return val;
}.property('rules.@each.priority'),
});

View File

@ -1,159 +1,121 @@
<div class="row">
<div class="col span-2 col-inline">
<label>{{t 'formBalancerRules.label'}}</label>
</div>
<div class="col span-8">
{{#unless editing}}
<button class="btn bg-link icon-btn" {{action "addRule" false}}>
<div class="clearfix">
<label class="acc-label">{{t 'formBalancerRules.label'}}</label>
{{#if editing}}
{{#if singleTarget}}
<button class="btn bg-link icon-btn" {{action "addRule" "target"}}>
<span class="darken"><i class="icon icon-plus text-small"></i></span>
<span>{{t 'formBalancerRules.addTargetLabel'}}</span>
</button>
{{else}}
<button class="btn bg-link icon-btn" {{action "addRule" "service"}}>
<span class="darken"><i class="icon icon-plus text-small"></i></span>
<span>{{t 'formBalancerRules.addServiceLabel'}}</span>
</button>
<button class="btn bg-link icon-btn pl-20" {{action "addRule" true}}>
<button class="btn bg-link icon-btn ml-20" {{action "addRule" "instance"}}>
<span class="darken"><i class="icon icon-plus text-small"></i></span>
<span>{{t 'formBalancerRules.addInstanceLabel'}}</span>
</button>
<button class="btn bg-link icon-btn ml-20" {{action "addRule" "selector"}}>
<span class="darken"><i class="icon icon-plus text-small"></i></span>
<span>{{t 'formBalancerRules.addSelectorLabel'}}</span>
</button>
{{/unless}}
</div>
</div>
<div class="row">
<div class="">
{{#if rules.length}}
<table class="grid fixed no-lines no-top-padding tight mb-0">
<thead>
<tr>
{{#if showListeners}}
<th width="30">&nbsp;</th>
<th width="100">{{t 'formBalancerRules.access.label'}}{{field-required}}</th>
{{#if showIp}}
<th>{{t 'formBalancerRules.sourceIp.label'}}</th>
{{/if}}
<th width="100">{{t 'formBalancerRules.protocol.label'}}{{field-required}}</th>
<th class="divided">{{t 'formBalancerRules.hostname.label'}}</th>
<th width="100">{{t 'formBalancerRules.sourcePort.label'}}{{field-required}}</th>
<th>{{t 'formBalancerRules.path.label'}}</th>
<th class="divided">{{t 'formBalancerRules.target'}}{{field-required}}</th>
<th width="100">{{t 'formBalancerRules.targetPort.label'}}{{field-required}}</th>
{{else}}
<th>{{t 'formBalancerRules.hostname.label'}}</th>
<th>{{t 'formBalancerRules.path.label'}}</th>
<th width="100" class="divided">{{t 'formBalancerRules.targetPort.label'}}{{field-required}}</th>
{{/if}}
{{#if showBackend}}
<th class="divided">{{t 'formBalancerRules.backendName.label'}}</th>
{{/if}}
{{#if showListeners}}
<th width="40">&nbsp;</th>
{{/if}}
</tr>
</thead>
<tbody>
{{#each rules as |rule idx|}}
<tr>
{{#if showListeners}}
<td data-title="{{t 'formBalancerRules.priority.label'}}">
<button class="btn bg-default btn-xs" {{action "moveUp" rule}} disabled={{eq rule.priority minPriority}}>
<i class="icon icon-chevron-up"></i>
</button>
<button class="btn bg-default btn-xs" {{action "moveDown" rule}} disabled={{eq rule.priority maxPriority}}>
<i class="icon icon-chevron-down"></i>
</button>
</td>
<td data-title="{{t 'formBalancerRules.access.label'}}">
<select class="form-control input-sm" onchange={{action (mut rule.access) value="target.value"}}>
<option value="public" selected={{eq rule.access "public"}}>{{t 'formBalancerRules.access.public'}}</option>
<option value="internal" selected={{eq rule.access "internal"}}>{{t 'formBalancerRules.access.internal'}}</option>
</select>
</td>
{{#if showIp}}
<td data-title="{{t 'formBalancerRules.sourceIp.label'}}">
{{#if (eq rule.access "public")}}
{{input type="text" class="form-control input-sm" value=rule.sourceIp placeholder=(t 'formBalancerRules.sourceIp.placeholder')}}
{{else}}
<span class="text-muted">{{t 'generic.na'}}</span>
{{/if}}
</td>
{{/if}}
<td data-title="{{t 'formBalancerRules.protocol.label'}}">
<select class="form-control input-sm" onchange={{action (mut rule.protocol) value="target.value"}}>
{{#each protocolChoices as |proto|}}
<option value={{proto}} selected={{eq rule.protocol proto}}>{{upper-case proto}}</option>
{{/each}}
</select>
</td>
{{/if}}
<td class="divided" data-title="{{t 'formBalancerRules.hostname.label'}}">
{{#if rule.canHostname}}
{{input type="text" class="form-control input-sm" value=rule.hostname placeholder=(t 'formBalancerRules.hostname.placeholder')}}
{{else}}
<span class="text-muted">{{t 'generic.na'}}</span>
{{/if}}
</td>
{{#if showListeners}}
<td data-title="{{t 'formBalancerRules.sourcePort.label'}}">
{{input-integer class="form-control input-sm" min="1" max="65535" value=rule.sourcePort placeholder=(t 'formBalancerRules.sourcePort.placeholder')}}
</td>
{{/if}}
<td data-title="{{t 'formBalancerRules.path.label'}}">
{{#if rule.canPath}}
{{input type="text" class="form-control input-sm" value=rule.path placeholder=(t 'formBalancerRules.path.placeholder')}}
{{else}}
<span class="text-muted">{{t 'generic.na'}}</span>
{{/if}}
</td>
{{#if showListeners}}
<td class="divided" data-title="{{t (if rule.isSelector 'formBalancerRules.selector.label' 'formBalancerRules.serviceId.label')}}">
{{#if rule.isSelector}}
{{input type="text" class="form-control input-sm" value=rule.selector placeholder=(t 'formBalancerRules.selector.placeholder')}}
{{else}}
{{schema/input-service selectClass="form-control input-sm" canBalanceTo=true selected=rule.serviceId}}
{{/if}}
</td>
{{/if}}
<td data-title="{{t 'formBalancerRules.targetPort.label'}}">
{{input-integer class="form-control input-sm" min="1" max="65535" value=rule.targetPort placeholder=(t 'formBalancerRules.targetPort.placeholder')}}
</td>
{{#if showBackend}}
<td class="divided" data-title="{{t 'formBalancerRules.backendName.label'}}">
{{input type="text" class="form-control input-sm" value=rule.backendName placeholder=(t 'formBalancerRules.backendName.placeholder')}}
</td>
{{/if}}
{{#if showListeners}}
<td class="text-right">
<button class="btn bg-primary btn-sm" {{action "removeRule" rule}}><i class="icon icon-minus"/><span class="sr-only">{{t 'generic.remove'}}</span></button>
</td>
{{/if}}
</tr>
{{/each}}
</tbody>
</table>
<p class="text-info">
{{t 'formBalancerRules.help.prefix'}}
{{#unless showBackend}}
<a href="#" {{action "showBackend"}}>
{{t 'formBalancerRules.help.showBackendLink'}}
</a>
{{/unless}}
{{#unless showIp}}
<a href="#" {{action "showIp"}}>
{{t 'formBalancerRules.help.showIpLink'}}
</a>
{{/unless}}
{{t 'formBalancerRules.help.suffix'}}
</p>
{{else}}
<span class="text-muted">{{t 'formBalancerRules.noRules'}}</span>
{{/if}}
</div>
{{/if}}
</div>
{{#if rules.length}}
<table class="grid fixed no-lines no-top-padding mb-0">
<thead>
<tr>
{{#unless singleTarget}}
<th width="30">&nbsp;</th>
{{/unless}}
<th class="divided">{{t 'formBalancerRules.hostname.label'}}</th>
<th>{{t 'formBalancerRules.path.label'}}</th>
{{#if singleTarget}}
<th width="30">&nbsp;</th>
{{else}}
<th class="divided">{{t 'formBalancerRules.target'}}{{field-required}}</th>
{{/if}}
<th width="100" class="divided">{{t 'formBalancerRules.targetPort.label'}}{{field-required}}</th>
{{#if showBackend}}
<th class="divided">{{t 'formBalancerRules.backendName.label'}}</th>
{{/if}}
{{#if editing}}
<th width="40">&nbsp;</th>
{{/if}}
</tr>
</thead>
<tbody>
{{#each rules as |rule idx|}}
<tr>
{{#unless singleTarget}}
<td data-title="{{t 'formBalancerRules.priority.label'}}">
<button class="btn bg-default btn-xs" {{action "moveUp" rule}} disabled={{eq idx 0}}>
<i class="icon icon-chevron-up"></i>
</button>
<button class="btn bg-default btn-xs" {{action "moveDown" rule}} disabled={{eq idx (sub rules.length 1)}}>
<i class="icon icon-chevron-down"></i>
</button>
</td>
{{/unless}}
<td class="divided" data-title="{{t 'formBalancerRules.hostname.label'}}">
{{#if rule.canHostname}}
{{input type="text" class="input-sm" value=rule.hostname placeholder=(t 'formBalancerRules.hostname.placeholder')}}
{{else}}
<span class="text-muted">{{t 'generic.na'}}</span>
{{/if}}
</td>
<td data-title="{{t 'formBalancerRules.path.label'}}">
{{#if rule.canPath}}
{{input type="text" class="input-sm" value=rule.path placeholder=(t 'formBalancerRules.path.placeholder')}}
{{else}}
<span class="text-muted">{{t 'generic.na'}}</span>
{{/if}}
</td>
{{#if singleTarget}}
<td>&nbsp;</td>
{{else}}
<td class="divided" data-title="{{t (concat-str 'formBalancerRules' rule.kind 'label' character='.')}}">
{{#if (eq rule.kind 'selector')}}
{{input type="text" class="input-sm" value=rule.selector placeholder=(t 'formBalancerRules.selector.placeholder')}}
{{else if (eq rule.kind 'instance')}}
{{schema/input-container selectClass="input-sm" selected=rule.instanceId}}
{{else}}
{{schema/input-service selectClass="input-sm" canBalanceTo=true selected=rule.serviceId}}
{{/if}}
</td>
{{/if}}
<td data-title="{{t 'formBalancerRules.targetPort.label'}}">
{{input-integer class="input-sm" min="1" max="65535" value=rule.targetPort placeholder=(t 'formBalancerRules.targetPort.placeholder')}}
</td>
{{#if showBackend}}
<td class="divided" data-title="{{t 'formBalancerRules.backendName.label'}}">
{{input type="text" class="input-sm" value=rule.backendName placeholder=(t 'formBalancerRules.backendName.placeholder')}}
</td>
{{/if}}
{{#if editing}}
<td class="text-right">
<button class="btn bg-primary btn-sm" {{action "removeRule" rule}}><i class="icon icon-minus"/><span class="sr-only">{{t 'generic.remove'}}</span></button>
</td>
{{/if}}
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<span class="text-muted">{{t 'formBalancerRules.noRules'}}</span>
{{/if}}

View File

@ -74,6 +74,10 @@ export default Ember.Component.extend({
count++;
}
if ( this.get('lbConfig.needsCertificate') && !count ) {
k = STATUS.INCOMPLETE;
}
if ( count ) {
k = STATUS.COUNTCONFIGURED;
}

View File

@ -0,0 +1,52 @@
import Ember from 'ember';
export default Ember.Component.extend({
allContainers : Ember.inject.service(),
selected: null, // Selected service ID
exclude: null, // ID or array of IDs to exclude from list
// For use as a catalog question
field: null, // Read default from a schema resourceField
value: null, // stackName/serviceName string output
init() {
this._super(...arguments);
let def = this.get('field.default');
if ( def && !this.get('selected') ) {
let match = this.get('allContainers.list').findBy('name', def);
this.set('selected', match || null);
}
},
grouped: function() {
let list = this.get('allContainers.list');
let exclude = this.get('exclude');
if ( exclude ) {
if ( !Ember.isArray(exclude) ) {
exclude = [exclude];
}
list = list.filter(x => !exclude.includes(x.id));
}
return this.get('allContainers').group(list);
}.property('allContainers.list.[]','canBalanceTo','canHaveContainers'),
selectedChanged: function() {
let id = this.get('selected');
let str = null;
if ( id ) {
let service = this.get('allContainers').byId(id);
if ( service ) {
str = service.get('stack.name') + '/' + service.get('name');
}
}
this.set('value', str);
}.observes('selected'),
});

View File

@ -0,0 +1,10 @@
<select class="{{selectClass}}" onchange={{action (mut selected) value="target.value"}}>
<option selected={{eq selected null}}>{{t 'schema.inputContainer.prompt'}}</option>
{{#each-in grouped as |group list|}}
<optgroup label={{group}}>
{{#each list as |svc|}}
<option selected={{eq selected svc.id}} value={{svc.id}}>{{svc.name}}</option>
{{/each}}
</optgroup>
{{/each-in}}
</select>

View File

@ -70,6 +70,5 @@ export default Ember.Component.extend({
}
this.set('value', str);
console.log('val', str);
}.observes('selected'),
});

View File

@ -51,8 +51,8 @@ export default Ember.Mixin.create({
// let version = instance.get('version')||"";
let k8sName = (instance.get('labels')||{})[C.LABEL.K8S_POD_NAMESPACE] || '';
let stackId = instance.get('primaryStack.id') || '';
let stackName = instance.get('primaryStack.displayName') || '';
let stackId = instance.get('stack.id') || '';
let stackName = instance.get('stack.displayName') || '';
let groupId, groupName;
if ( k8sName ) {

View File

@ -30,17 +30,13 @@ var Container = Instance.extend({
primaryHost : denormalizeId('hostId'),
services : denormalizeIdArray('serviceIds'),
primaryService : Ember.computed.alias('services.firstObject'),
primaryStack : Ember.computed.alias('primaryService.stack'),
referencedStack : denormalizeId('stackId'),
referencedService : denormalizeId('serviceId'),
service: Ember.computed('primaryService','referencedService', function() {
return this.get('referencedService') || this.get('primaryService');
}),
stack: Ember.computed('primaryStack','referencedStack', function() {
return this.get('referencedStack') || this.get('primaryStack');
}),
stack: denormalizeId('stackId'),
actions: {
restart: function() {

View File

@ -2,8 +2,11 @@ import Ember from 'ember';
import Resource from 'ember-api-store/models/resource';
import C from 'ui/utils/constants';
import { formatSi } from 'ui/utils/util';
import { denormalizeId } from 'ember-api-store/utils/denormalize';
export default Resource.extend({
stack: denormalizeId('stackId'),
isSystem: function() {
if ( this.get('system') ) {
return true;

View File

@ -27,10 +27,6 @@ var LoadBalancerService = Service.extend({
let publish = this.get('launchConfig.ports')||[];
publish.forEach((str) => {
let spec = parsePortSpec(str,'tcp');
if ( !spec.hostPort || spec.hostIp ) {
this.set('hasUnsupportedPorts', true);
}
if ( spec.hostPort ) {
rules.filterBy('sourcePort', spec.hostPort).forEach((rule) => {
rule.set('access', 'public');

View File

@ -0,0 +1,57 @@
import Ember from 'ember';
export default Ember.Service.extend({
intl: Ember.inject.service(),
store: Ember.inject.service(),
prefs: Ember.inject.service(),
list: function() {
let intl = this.get('intl');
let showSystem = this.get('prefs.showSystemResources');
return this.get('_allInstances').filter((inst) => !inst.get('serviceId') && (!inst.get('isSystem') || showSystem)).map((inst) => {
let stackName = 'Standalone';
if ( inst.get('stack') ) {
stackName = inst.get('stack.displayName') || '('+inst.get('stackId')+')';
}
return {
group: intl.t('allServices.stackGroup', {name: stackName}),
id: inst.get('id'),
stackName: stackName,
name: inst.get('displayName'),
obj: inst,
};
});
}.property('_allInstances.@each.{id,system,displayName}','prefs.showSystemResources'),
grouped: function() {
return this.group(this.get('list'));
}.property('list.[]'),
group(list) {
let out = {};
list.slice().sortBy('group','name','id').forEach((inst) => {
let ary = out[inst.group];
if( !ary ) {
ary = [];
out[inst.group] = ary;
}
ary.push(inst);
});
return out;
},
_allInstances: function() {
let store = this.get('store');
store.find('instance');
return store.all('instance');
}.property(),
byId(id) {
return this.get('store').getById('instance', id);
},
});

View File

@ -8,7 +8,12 @@ export default Ember.Service.extend({
settings: Ember.inject.service(),
absolute: function() {
var url = this.get('app.apiServer');
let setting = this.get(`settings.${C.SETTING.API_HOST}`);
if ( setting && setting.indexOf('http') !== 0 ) {
setting = 'http://' + setting;
}
let url = setting || this.get('app.apiServer');
// If the URL is relative, add on the current base URL from the browser
if ( url.indexOf('http') !== 0 )
@ -20,7 +25,7 @@ export default Ember.Service.extend({
url = url.replace(/\/+$/,'') + '/';
return url;
}.property('app.apiServer'),
}.property(`settings.${C.SETTING.API_HOST}`,'app.apiServer'),
host: function() {
var a = document.createElement('a');

View File

@ -20,14 +20,14 @@
<h1>{{t 'signupPage.header'}}</h1>
<form class="form text-left" {{action "register" on='submit'}}>
<div class="row">
<label for="login-user-name">{{t 'signupPage.form.labels.loginUsername'}}</label>
<label for="login-user-name">{{t 'signupPage.form.labels.loginUsername'}}{{field-required}}</label>
<div name="login-user-name">
{{input type="text" value=model.name}}
</div>
</div>
<div class="row inline-form pt-15 pb-30">
<label for="email">{{t 'signupPage.form.labels.email'}}</label>
<label for="email">{{t 'signupPage.form.labels.email'}}{{field-required}}</label>
<div name="email">
{{input type="email" value=model.email}}
</div>
@ -38,4 +38,4 @@
</form>
</div>
{{/if}}
{{/authorize-user}}
{{/authorize-user}}

View File

@ -1,6 +1,8 @@
import Ember from 'ember';
export default Ember.Controller.extend({
prefs: Ember.inject.service(),
showAddtlInfo: false,
selectedService: null,
@ -61,14 +63,6 @@ export default Ember.Controller.extend({
},
],
stackContainers: Ember.computed('model.stack.services.@each.healthState', function() {
var neu = [];
this.get('model.stack.services').forEach((service) => {
neu = neu.concat(service.get('instances'));
});
return neu;
}),
getType(ownType, real=true) {
return this.get('model.services').filter((service) => {
if (real ? (service.get('isReal') && service.get('kind') === ownType) : (service.get('kind') === ownType)) {
@ -78,18 +72,28 @@ export default Ember.Controller.extend({
});
},
scalingGroups: Ember.computed('model.services.@each.healthState', function() {
scalingGroups: Ember.computed('model.services.[]', function() {
return this.getType('scalingGroup');
}),
loadBalancers: Ember.computed('model.services.@each.healthState', function() {
loadBalancers: Ember.computed('model.services.[]', function() {
return this.getType('loadBalancerService');
}),
dnsServices: Ember.computed('model.services.@each.healthState', function() {
dnsServices: Ember.computed('model.services.[]', function() {
return this.getType('dnsService', false).concat(this.getType('externalService', false));
}),
instances: Ember.computed('model.instances.[]','prefs.showSystemResources', function() {
let out = this.get('model.instances').filterBy('stackId', this.get('model.stack.id'));
out = out.filterBy('serviceId', null);
if ( !this.get('prefs.showSystemResources') ) {
out = out.filterBy('isSystem', false);
}
return out;
}),
instanceCount: function() {
var count = 0;
(this.get('model.stack.services')||[]).forEach((service) => {

View File

@ -12,15 +12,15 @@
{{#accordion-list-item
title=(t 'stackPage.containers.header')
detail=(t 'stackPage.containers.detail')
status=(t 'stackPage.containers.status' count=model.instances.length)
statusClass=(if model.instances.length 'bg-success' 'text-muted')
status=(t 'stackPage.containers.status' count=instances.length)
statusClass=(if instances.length 'bg-success' 'text-muted')
expandAll=al.expandAll
expand=(action expandFn)
componentName='container-table'
as | parent |
}}
{{component parent.intent
body=model.instances
body=instances
search=true
sortBy=sortBy
stickyHeader=false

View File

@ -3,16 +3,10 @@ import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
var store = this.get('store');
var all = this.modelFor('stacks');
return store.find('stack', params.stack_id).then((stack) => {
var neu = [];
stack.get('services').forEach((service) => {
neu = neu.concat(service.get('instances'));
});
return Ember.Object.create({
stack: stack,
all: all,
instances: neu,
instances: store.all('instance'),
services: stack.get('services')
});
});

View File

@ -112,7 +112,7 @@ textarea {
.form-control-static {
line-height: 24px;
padding: 5px 0;
padding: 3px 0;
border: 2px solid transparent;
}

View File

@ -6,7 +6,7 @@
<h2>{{t 'verifyPage.subtext'}}</h2>
<form class="form text-left" {{action 'createAcct' on="submit"}}>
<div class="inline-form pt-15 pb-30">
<label for="login-user-name">{{t 'signupPage.form.labels.loginUsername'}}</label>
<label for="login-user-name">{{t 'signupPage.form.labels.loginUsername'}}{{field-required}}</label>
<div>
{{input type="text" value=model.name id="login-user-name"}}
</div>
@ -40,4 +40,4 @@
</div>
</div>
{{/unless}}
{{/authorize-user}}
{{/authorize-user}}

View File

@ -1170,7 +1170,7 @@ stackPage:
backLink: Back to all stacks
containers:
header: Containers
detail: A list of containers in this stack
detail: Standalone Containers that are not part of a Scaling Group or Load Balancer
status: |
{count, plural,
=0 {No containers}
@ -1179,7 +1179,7 @@ stackPage:
}
scalingGroups:
header: Scaling Groups
detail: 'A list of scaling groups this stack is attached to.'
detail: ''
status: |
{count, plural,
=0 {No groups}
@ -1188,7 +1188,7 @@ stackPage:
}
loadBalancers:
header: Load Balancers
detail: 'A list of load balancers in this stack'
detail: ''
status: |
{count, plural,
=0 {No balancers}
@ -1196,13 +1196,13 @@ stackPage:
other {# balancers}
}
dnsServices:
header: DNS Services
detail: 'A list of DNS Services in this stack'
header: DNS Entries
detail: ''
status: |
{count, plural,
=0 {No rules}
=1 {# rule}
other {# rules}
=0 {No entries}
=1 {# entry}
other {# entries}
}
newStack:
@ -1915,19 +1915,20 @@ formBalancerConfig:
See <a href="https://cbonte.github.io/haproxy-dconv/1.6/configuration.html" target="_blank" rel="nofollow noopener">haproxy documentation</a> fore more info about specific options that can go into the the config file. When overriding the <code>backend</code> or similar lines which include the IP address of the target container, use <code>$IP</code> where the address goes and {appName} will generate the appropriate line(s).
config:
prompt: Custom haproxy.cfg content
formBalancerRules:
label: Port Rules
detail: 'These properties show the port mapping details of your container'
formBalancerListeners:
label: Listeners & Target Rules
detail: Control the mapping of requests coming into the balancer to the desired target.
status: |
{count, plural,
=0 {No rules}
=1 {# rule}
other {# rules}
}
noRules: No Rules
addServiceLabel: Add Scaling Group Rule
addSelectorLabel: Add Selector Rule
target: Target
noRules: No Ports
addPortLabel: Add a Listening Port
removePortLabel: Remove this Listening Port
showBackendLabel: "Customize backend names"
access:
label: Access
public: Public
@ -1936,10 +1937,19 @@ formBalancerRules:
label: Protocol
sourceIp:
label: Host IP
placeholder: e.g. 1.2.3.4
placeholder: "e.g. 1.2.3.4; Default: All"
sourcePort:
label: Port
label: Listening Port
placeholder: e.g. 80
help: "Host and Path rules are matched top-to-bottom in the order shown. Backends will be named randomly by default; to customize the generated backends, provide a name and then refer to that name in your custom haproxy.cfg."
formBalancerRules:
label: Target Rules
noRules: No Rules
addServiceLabel: Add a Service
addInstanceLabel: Add a Container
addSelectorLabel: Add a Selector
addTargetLabel: Add a Rule
path:
label: Path
placeholder: e.g. /foo
@ -1956,16 +1966,14 @@ formBalancerRules:
label: Priority
moveUp: Move Up
moveDown: Move Down
serviceId:
target: Target
container:
label: Container
service:
label: Service
selector:
label: Selector
placeholder: e.g. foo=bar
help:
prefix: "Host and Path rules are matched top-to-bottom in the order shown. Backends will be named randomly by default; to customize the generated backends, provide a name and then refer to that in the custom haproxy.cfg. "
showBackendLink: "Show custom backend names."
showIpLink: "Show host IP address options."
suffix: ""
formCloudHost:
title: Host
@ -2101,7 +2109,7 @@ formHealthCheck:
label: Host Header
placeholder: e.g. www.example.com
port:
label: Port
label: Listening Port
placeholder: e.g. 80
initializingTimeout:
label: Initializing Timeout
@ -2431,7 +2439,7 @@ formStickiness:
indirect: Indirect
sendHeader: Send no-cache header
onPost: Only set cookie on POST
noPorts: There are no HTTP Port Rules configured.
noPorts: There are no HTTP Listeners configured.
placeholder:
sticky: e.g. sticky
@ -3360,12 +3368,13 @@ newBalancer:
edit: Edit Load Balancer
upgrade: Upgrade Load Balancer
error:
noRules: "Choose one or more port rules to listen on"
noRules: "You must have one or more listening ports and target rules"
noSourcePort: "Source Port is required on each rule"
invalidSourcePort: "Invalid source port: '{num}'"
invalidTargetPort: "Invalid target port: '{num}'"
mixedPort: "Port {num} has multiple rules with conflicting access/protcols"
noTarget: "Target is required on each rule"
noTargetPort: "Target Port is required on each rule"
needsCertificate: "A certificate is required because there are SSL/TLS port rules"
newCatalog:
@ -3614,10 +3623,12 @@ schema:
n: "False"
inputCertificate:
prompt: Choose a Certificate...
inputContainer:
prompt: Choose a Container...
inputEnum:
option: Choose an option...
inputHost:
label: Select Host
label: Choose a Host...
inputService:
prompt: Choose a Service...
inputSecret: