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')); let extInfo = parseExternalId(stack.get('externalId'));
deps.push(catalog.fetchTemplate(extInfo.templateId, false)); deps.push(catalog.fetchTemplate(extInfo.templateId, false));
}); });
return Ember.RSVP.all(deps).then((ahray) => {
ahray.forEach((ary) => { return Ember.RSVP.all(deps).then((templates) => {
let stck = stacks.findBy('externalIdInfo.templateId', ary.id); templates.forEach((template) => {
stck.catalogTemplateInfo = ary; // need that generica catalog icon stacks.filterBy('externalIdInfo.templateId', template.id).forEach((stack) => {
Ember.set(stack,'catalogTemplateInfo',template);
});
}); });
return Ember.Object.create({ return Ember.Object.create({
stacks: stacks, stacks: stacks,
}); });

View File

@ -62,14 +62,6 @@
{{/if}} {{/if}}
{{/sortable-table}} {{/sortable-table}}
{{else}} {{else}}
<div class="row"> {{empty-table resource="container" newRoute="balancers.new" newTranslationKey="nav.containers.addBalancer"}}
<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>
{{/if}} {{/if}}
</section> </section>

View File

@ -9,7 +9,7 @@ export default Ember.Controller.extend({
actions: { actions: {
cancel() { 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, isGlobal : null,
isRequestedHost : null, isRequestedHost : null,
upgradeOptions : null, upgradeOptions : null,
hasUnsupportedPorts : false,
// Errors from components // Errors from components
ruleErrors : null, ruleErrors : null,
@ -30,7 +29,6 @@ export default Ember.Component.extend(NewOrEdit, {
this._super(...arguments); this._super(...arguments);
this.labelsChanged(); this.labelsChanged();
this.get('service').initPorts(); this.get('service').initPorts();
this.updatePorts();
}, },
actions: { actions: {
@ -64,119 +62,6 @@ export default Ember.Component.extend(NewOrEdit, {
return this.get('intl').t(k); return this.get('intl').t(k);
}.property('intl.locale','needsUpgrade','isService','isVm','service.secondaryLaunchConfigs.length'), }.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() { needsUpgrade: function() {
function arrayToStr(map) { function arrayToStr(map) {
map = map || {}; map = map || {};
@ -278,8 +163,6 @@ export default Ember.Component.extend(NewOrEdit, {
// Save // Save
// ---------------------------------- // ----------------------------------
willSave() { willSave() {
this.validateRules();
let ok = this._super(...arguments); let ok = this._super(...arguments);
if ( ok && !this.get('isUpgrade') ) { if ( ok && !this.get('isUpgrade') ) {
// Set the stack ID // Set the stack ID

View File

@ -34,9 +34,11 @@
</div> </div>
<section class="horizontal-form container-fluid"> <section class="horizontal-form container-fluid">
{{form-balancer-rules {{form-balancer-listeners
service=service service=service
errors=ruleErrors
}} }}
<hr/>
</section> </section>
{{container/form-scheduling {{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 Ember from 'ember';
import { parsePortSpec } from 'ui/utils/parse-port';
export default Ember.Component.extend({ export default Ember.Component.extend({
intl: Ember.inject.service(),
service: null,
ruleType: 'portRule',
showListeners: Ember.computed.equal('ruleType','portRule'),
rules: null, rules: null,
protocolChoices: null, singleTarget: true,
showBackend: null, protocol: null,
showIp: null, editing: true,
onInit: function() { ruleType: 'portRule',
let rules = this.get('service.lbConfig.portRules');
if ( !rules ) {
rules = [];
this.set('service.lbConfig.portRules', rules);
}
rules.forEach((rule) => { rulesChanged: function() {
rule.isSelector = !!rule.selector; this.sendAction('rulesChanged');
}); }.observes('rules.@each.{hostname,path,kind,instanceId,serviceId,selector,targetPort,backendName}'),
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'),
actions: { actions: {
addRule(isSelector) { addRule(kind) {
let max = 0; let max = 0;
let rules = this.get('rules'); let rules = this.get('rules');
rules.forEach((rule) => { rules.forEach((rule) => {
@ -59,9 +22,8 @@ export default Ember.Component.extend({
rules.pushObject(this.get('store').createRecord({ rules.pushObject(this.get('store').createRecord({
type: this.get('ruleType'), type: this.get('ruleType'),
access: 'public', kind: kind,
isSelector: isSelector, protocol: this.get('protocol'),
protocol: 'http',
priority: max+1, priority: max+1,
})); }));
}, },
@ -93,16 +55,15 @@ export default Ember.Component.extend({
removeRule(rule) { removeRule(rule) {
this.get('rules').removeObject(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() { updatePriorities() {
let pri = 1; let pri = 1;
this.get('rules').forEach((rule) => { this.get('rules').forEach((rule) => {
@ -110,27 +71,4 @@ export default Ember.Component.extend({
pri++; 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="clearfix">
<div class="col span-2 col-inline"> <label class="acc-label">{{t 'formBalancerRules.label'}}</label>
<label>{{t 'formBalancerRules.label'}}</label> {{#if editing}}
</div> {{#if singleTarget}}
<div class="col span-8"> <button class="btn bg-link icon-btn" {{action "addRule" "target"}}>
{{#unless editing}} <span class="darken"><i class="icon icon-plus text-small"></i></span>
<button class="btn bg-link icon-btn" {{action "addRule" false}}> <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 class="darken"><i class="icon icon-plus text-small"></i></span>
<span>{{t 'formBalancerRules.addServiceLabel'}}</span> <span>{{t 'formBalancerRules.addServiceLabel'}}</span>
</button> </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 class="darken"><i class="icon icon-plus text-small"></i></span>
<span>{{t 'formBalancerRules.addSelectorLabel'}}</span> <span>{{t 'formBalancerRules.addSelectorLabel'}}</span>
</button> </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}} {{/if}}
</div> {{/if}}
</div> </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++; count++;
} }
if ( this.get('lbConfig.needsCertificate') && !count ) {
k = STATUS.INCOMPLETE;
}
if ( count ) { if ( count ) {
k = STATUS.COUNTCONFIGURED; 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); this.set('value', str);
console.log('val', str);
}.observes('selected'), }.observes('selected'),
}); });

View File

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

View File

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

View File

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

View File

@ -27,10 +27,6 @@ var LoadBalancerService = Service.extend({
let publish = this.get('launchConfig.ports')||[]; let publish = this.get('launchConfig.ports')||[];
publish.forEach((str) => { publish.forEach((str) => {
let spec = parsePortSpec(str,'tcp'); let spec = parsePortSpec(str,'tcp');
if ( !spec.hostPort || spec.hostIp ) {
this.set('hasUnsupportedPorts', true);
}
if ( spec.hostPort ) { if ( spec.hostPort ) {
rules.filterBy('sourcePort', spec.hostPort).forEach((rule) => { rules.filterBy('sourcePort', spec.hostPort).forEach((rule) => {
rule.set('access', 'public'); 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(), settings: Ember.inject.service(),
absolute: function() { 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 the URL is relative, add on the current base URL from the browser
if ( url.indexOf('http') !== 0 ) if ( url.indexOf('http') !== 0 )
@ -20,7 +25,7 @@ export default Ember.Service.extend({
url = url.replace(/\/+$/,'') + '/'; url = url.replace(/\/+$/,'') + '/';
return url; return url;
}.property('app.apiServer'), }.property(`settings.${C.SETTING.API_HOST}`,'app.apiServer'),
host: function() { host: function() {
var a = document.createElement('a'); var a = document.createElement('a');

View File

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

View File

@ -1,6 +1,8 @@
import Ember from 'ember'; import Ember from 'ember';
export default Ember.Controller.extend({ export default Ember.Controller.extend({
prefs: Ember.inject.service(),
showAddtlInfo: false, showAddtlInfo: false,
selectedService: null, 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) { getType(ownType, real=true) {
return this.get('model.services').filter((service) => { return this.get('model.services').filter((service) => {
if (real ? (service.get('isReal') && service.get('kind') === ownType) : (service.get('kind') === ownType)) { 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'); return this.getType('scalingGroup');
}), }),
loadBalancers: Ember.computed('model.services.@each.healthState', function() { loadBalancers: Ember.computed('model.services.[]', function() {
return this.getType('loadBalancerService'); 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)); 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() { instanceCount: function() {
var count = 0; var count = 0;
(this.get('model.stack.services')||[]).forEach((service) => { (this.get('model.stack.services')||[]).forEach((service) => {

View File

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

View File

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

View File

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

View File

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

View File

@ -1170,7 +1170,7 @@ stackPage:
backLink: Back to all stacks backLink: Back to all stacks
containers: containers:
header: 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: | status: |
{count, plural, {count, plural,
=0 {No containers} =0 {No containers}
@ -1179,7 +1179,7 @@ stackPage:
} }
scalingGroups: scalingGroups:
header: Scaling Groups header: Scaling Groups
detail: 'A list of scaling groups this stack is attached to.' detail: ''
status: | status: |
{count, plural, {count, plural,
=0 {No groups} =0 {No groups}
@ -1188,7 +1188,7 @@ stackPage:
} }
loadBalancers: loadBalancers:
header: Load Balancers header: Load Balancers
detail: 'A list of load balancers in this stack' detail: ''
status: | status: |
{count, plural, {count, plural,
=0 {No balancers} =0 {No balancers}
@ -1196,13 +1196,13 @@ stackPage:
other {# balancers} other {# balancers}
} }
dnsServices: dnsServices:
header: DNS Services header: DNS Entries
detail: 'A list of DNS Services in this stack' detail: ''
status: | status: |
{count, plural, {count, plural,
=0 {No rules} =0 {No entries}
=1 {# rule} =1 {# entry}
other {# rules} other {# entries}
} }
newStack: 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). 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: config:
prompt: Custom haproxy.cfg content prompt: Custom haproxy.cfg content
formBalancerRules:
label: Port Rules formBalancerListeners:
detail: 'These properties show the port mapping details of your container' label: Listeners & Target Rules
detail: Control the mapping of requests coming into the balancer to the desired target.
status: | status: |
{count, plural, {count, plural,
=0 {No rules} =0 {No rules}
=1 {# rule} =1 {# rule}
other {# rules} other {# rules}
} }
noRules: No Rules noRules: No Ports
addServiceLabel: Add Scaling Group Rule addPortLabel: Add a Listening Port
addSelectorLabel: Add Selector Rule removePortLabel: Remove this Listening Port
target: Target showBackendLabel: "Customize backend names"
access: access:
label: Access label: Access
public: Public public: Public
@ -1936,10 +1937,19 @@ formBalancerRules:
label: Protocol label: Protocol
sourceIp: sourceIp:
label: Host IP label: Host IP
placeholder: e.g. 1.2.3.4 placeholder: "e.g. 1.2.3.4; Default: All"
sourcePort: sourcePort:
label: Port label: Listening Port
placeholder: e.g. 80 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: path:
label: Path label: Path
placeholder: e.g. /foo placeholder: e.g. /foo
@ -1956,16 +1966,14 @@ formBalancerRules:
label: Priority label: Priority
moveUp: Move Up moveUp: Move Up
moveDown: Move Down moveDown: Move Down
serviceId: target: Target
container:
label: Container
service:
label: Service label: Service
selector: selector:
label: Selector label: Selector
placeholder: e.g. foo=bar 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: formCloudHost:
title: Host title: Host
@ -2101,7 +2109,7 @@ formHealthCheck:
label: Host Header label: Host Header
placeholder: e.g. www.example.com placeholder: e.g. www.example.com
port: port:
label: Port label: Listening Port
placeholder: e.g. 80 placeholder: e.g. 80
initializingTimeout: initializingTimeout:
label: Initializing Timeout label: Initializing Timeout
@ -2431,7 +2439,7 @@ formStickiness:
indirect: Indirect indirect: Indirect
sendHeader: Send no-cache header sendHeader: Send no-cache header
onPost: Only set cookie on POST onPost: Only set cookie on POST
noPorts: There are no HTTP Port Rules configured. noPorts: There are no HTTP Listeners configured.
placeholder: placeholder:
sticky: e.g. sticky sticky: e.g. sticky
@ -3360,12 +3368,13 @@ newBalancer:
edit: Edit Load Balancer edit: Edit Load Balancer
upgrade: Upgrade Load Balancer upgrade: Upgrade Load Balancer
error: 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" noSourcePort: "Source Port is required on each rule"
invalidSourcePort: "Invalid source port: '{num}'" invalidSourcePort: "Invalid source port: '{num}'"
invalidTargetPort: "Invalid target port: '{num}'" invalidTargetPort: "Invalid target port: '{num}'"
mixedPort: "Port {num} has multiple rules with conflicting access/protcols" mixedPort: "Port {num} has multiple rules with conflicting access/protcols"
noTarget: "Target is required on each rule" 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" needsCertificate: "A certificate is required because there are SSL/TLS port rules"
newCatalog: newCatalog:
@ -3614,10 +3623,12 @@ schema:
n: "False" n: "False"
inputCertificate: inputCertificate:
prompt: Choose a Certificate... prompt: Choose a Certificate...
inputContainer:
prompt: Choose a Container...
inputEnum: inputEnum:
option: Choose an option... option: Choose an option...
inputHost: inputHost:
label: Select Host label: Choose a Host...
inputService: inputService:
prompt: Choose a Service... prompt: Choose a Service...
inputSecret: inputSecret: