Merge pull request #145 from vincent99/service-balancer

Load Balancer Service
This commit is contained in:
Vincent Fiduccia 2015-05-05 11:42:39 -07:00
commit b7e4a28e64
17 changed files with 536 additions and 34 deletions

View File

@ -7,6 +7,6 @@ export default Ember.Component.extend({
tagName: 'nav', tagName: 'nav',
hasServices: function() { hasServices: function() {
var store = this.get('store'); var store = this.get('store');
return store && store.hasRecordFor('schema','service') && this.get('session.showServices'); return store && store.hasRecordFor('schema','service');
}.property(), }.property(),
}); });

View File

@ -21,6 +21,14 @@ var EnvironmentController = Cattle.TransitioningResourceController.extend({
}); });
}, },
addBalancer: function() {
this.transitionToRoute('service.new-balancer', {
queryParams: {
environmentId: this.get('id'),
},
});
},
edit: function() { edit: function() {
this.transitionToRoute('environment.edit', this.get('id')); this.transitionToRoute('environment.edit', this.get('id'));
}, },

View File

@ -6,5 +6,8 @@ export default Ember.ObjectController.extend({
addService: function() { addService: function() {
this.get('controllers.environment').send('addService'); this.get('controllers.environment').send('addService');
}, },
addBalancer: function() {
this.get('controllers.environment').send('addBalancer');
},
}, },
}); });

View File

@ -8,7 +8,12 @@
<div class="pod-column"> <div class="pod-column">
{{#each item in col}} {{#each item in col}}
{{#if item.isNewPlaceHolder}} {{#if item.isNewPlaceHolder}}
{{add-pod action="addService" label="Add Service"}} {{#if item.isService}}
{{add-pod action="addService" label="Add Service"}}
{{/if}}
{{#if item.isBalancer}}
{{add-pod action="addBalancer" label="Add Load Balancer"}}
{{/if}}
{{else}} {{else}}
{{#with item as service controller="service"}} {{#with item as service controller="service"}}
{{service-pod model=service}} {{service-pod model=service}}

View File

@ -22,8 +22,9 @@ export default ColumnView.extend({
columns[nextIndex()].push(services.objectAt(i)); columns[nextIndex()].push(services.objectAt(i));
} }
// Add a placeholder for where to put the 'Add Service' button // Add a placeholder for where to put the 'Add Service' and 'Add Balancer' buttons
columns[nextIndex()].push(Ember.Object.create({isNewPlaceHolder: true})); columns[nextIndex()].push(Ember.Object.create({isNewPlaceHolder: true, isService: true}));
columns[nextIndex()].push(Ember.Object.create({isNewPlaceHolder: true, isBalancer: true}));
this.set('podCount', podCount); this.set('podCount', podCount);

View File

@ -0,0 +1,59 @@
import Cattle from 'ui/utils/cattle';
var LoadBalancerServiceController = Cattle.TransitioningResourceController.extend({
actions: {
activate: function() {
return this.doAction('activate');
},
deactivate: function() {
return this.doAction('deactivate');
},
edit: function() {
this.transitionToRoute('service.edit', this.get('id'));
},
scaleUp: function() {
this.incrementProperty('scale');
return this.save();
}
},
availableActions: function() {
var a = this.get('actions');
var choices = [
{ label: 'Start', icon: 'ss-play', action: 'activate', enabled: !!a.activate, color: 'text-success'},
{ label: 'Stop', icon: 'ss-pause', action: 'deactivate', enabled: !!a.deactivate, color: 'text-danger'},
{ label: 'Delete', icon: 'ss-trash', action: 'promptDelete', enabled: !!a.remove, altAction: 'delete', color: 'text-warning' },
{ label: 'Purge', icon: 'ss-tornado', action: 'purge', enabled: !!a.purge },
{ divider: true },
{ label: 'View in API', icon: '', action: 'goToApi', enabled: true},
{ divider: true },
{ label: 'Edit', icon: 'ss-write', action: 'edit', enabled: !!a.update },
];
return choices;
}.property('actions.{activate,deactivate,update,remove,purge}'),
getEnvironment: function() {
return this.get('store').find('environment', this.get('environmentId'));
},
});
LoadBalancerServiceController.reopenClass({
stateMap: {
'requested': {icon: 'ss-tag', color: 'text-danger'},
'registering': {icon: 'ss-tag', color: 'text-danger'},
'activating': {icon: 'ss-tag', color: 'text-danger'},
'active': {icon: 'ss-layergroup', color: 'text-success'},
'deactivating': {icon: 'ss-down', color: 'text-danger'},
'inactive': {icon: 'fa fa-circle', color: 'text-danger'},
'removing': {icon: 'ss-trash', color: 'text-danger'},
'removed': {icon: 'ss-trash', color: 'text-danger'},
}
});
export default LoadBalancerServiceController;

View File

@ -0,0 +1,15 @@
import Cattle from 'ui/utils/cattle';
var LoadBalancerService = Cattle.TransitioningResource.extend({
type: 'service',
consumedServicesUpdated: 0,
onConsumedServicesChanged: function() {
this.incrementProperty('consumedServicesUpdated');
}.observes('consumedservices.@each.{id,name,state}'),
});
LoadBalancerService.reopenClass({
});
export default LoadBalancerService;

View File

@ -112,6 +112,7 @@ Router.map(function() {
this.resource('environments.new', {path: '/environments/add'}); this.resource('environments.new', {path: '/environments/add'});
this.resource('service.new', {path: '/environments/add-service'}); this.resource('service.new', {path: '/environments/add-service'});
this.resource('service.new-balancer', {path: '/environments/add-balancer'});
this.resource('environments', {path: '/environments'}, function() { this.resource('environments', {path: '/environments'}, function() {
this.route('index', {path: '/'}); this.route('index', {path: '/'});
this.resource('environment', {path: '/:environment_id'}, function() { this.resource('environment', {path: '/:environment_id'}, function() {

View File

@ -49,6 +49,8 @@ ServiceController.reopenClass({
'registering': {icon: 'ss-tag', color: 'text-danger'}, 'registering': {icon: 'ss-tag', color: 'text-danger'},
'activating': {icon: 'ss-tag', color: 'text-danger'}, 'activating': {icon: 'ss-tag', color: 'text-danger'},
'active': {icon: 'ss-layergroup', color: 'text-success'}, 'active': {icon: 'ss-layergroup', color: 'text-success'},
'updating-active': {icon: 'ss-tag', color: 'text-success'},
'updating-inactive':{icon: 'ss-tag', color: 'text-danger'},
'deactivating': {icon: 'ss-down', color: 'text-danger'}, 'deactivating': {icon: 'ss-down', color: 'text-danger'},
'inactive': {icon: 'fa fa-circle', color: 'text-danger'}, 'inactive': {icon: 'fa fa-circle', color: 'text-danger'},
'removing': {icon: 'ss-trash', color: 'text-danger'}, 'removing': {icon: 'ss-trash', color: 'text-danger'},

View File

@ -0,0 +1,160 @@
import Ember from 'ember';
import Cattle from 'ui/utils/cattle';
import EditLoadBalancerConfig from 'ui/mixins/edit-loadbalancerconfig';
export default Ember.ObjectController.extend(Cattle.NewOrEditMixin, EditLoadBalancerConfig, {
queryParams: ['environmentId','tab'],
environmentId: null,
tab: 'listeners',
error: null,
editing: false,
primaryResource: Ember.computed.alias('model.balancer'),
actions: {
addTargetService: function() {
this.get('targetsArray').pushObject({isService: true, value: null});
},
removeTarget: function(obj) {
this.get('targetsArray').removeObject(obj);
},
},
initFields: function() {
this._super();
this.set('targetsArray', [{isService: true, value: null}]);
this.set('listenersArray', [
this.get('store').createRecord({
type: 'loadBalancerListener',
name: 'uilistener',
sourcePort: '',
sourceProtocol: 'http',
targetPort: '',
targetProtocol: 'http',
algorithm: 'roundrobin',
})
]);
this.initUri();
},
useExisting: 'no',
isUseExisting: Ember.computed.equal('useExisting','yes'),
hasNoExisting: Ember.computed.equal('activeConfigs.length',0),
existingConfigId: null,
initHosts: function() {
},
hostDisabled: Ember.computed.equal('hostChoices.length',0),
hostChoices: function() {
return this.get('allHosts').filter((host) => {
return host.get('state') === 'active';
}).sortBy('name','id');
}.property('allHosts.@each.{id,name,state}'),
targetsArray: null,
targetServiceIds: function() {
return this.get('targetsArray').filterProperty('isService',true).filterProperty('value').map((choice) => {
return Ember.get(choice,'value');
}).uniq();
}.property('targetsArray.@each.{isService,value}'),
targetChoices: function() {
var list = [];
var env = this.get('environment');
var envName = env.get('name') || ('(Environment '+env.get('id')+')');
env.get('services').map((service) => {
list.pushObject({
group: 'Environment: ' + envName,
id: service.get('id'),
name: service.get('name') || ('(' + service.get('id') + ')')
});
});
return list.sortBy('group','name','id');
}.property('environment.services.@each.{name,id},environment.{name,id}').volatile(),
activeConfigs: function() {
return this.get('allConfigs').filter((config) => {
return config.get('state') === 'active';
});
}.property('allConfigs.@each.state'),
validate: function() {
this._super();
var errors = this.get('errors')||[];
if ( !this.get('targetServiceIds.length') )
{
errors.push('Choose one or more targets to send traffic to');
}
if (!this.get('listenersArray.length') )
{
errors.push('One or more listening ports are required');
}
errors.pushObjects(this.get('config').validationErrors());
this.get('listenersArray').forEach((listener) => {
errors.pushObjects(listener.validationErrors());
});
if ( (this.get('listenersArray')||[]).filterProperty('sourcePort',8080).get('length') > 0 )
{
errors.push('Port 8080 cannot currently be used as a source port');
}
errors.pushObjects(this.get('balancer').validationErrors());
if ( errors.length )
{
this.set('errors',errors.uniq());
return false;
}
return true;
},
has8080: function() {
// The port might be an int or a string due to validation..
return ( (this.get('listenersArray')||[]).filterProperty('sourcePort','8080').get('length') > 0 ) ||
( (this.get('listenersArray')||[]).filterProperty('sourcePort',8080).get('length') > 0 );
}.property('listenersArray.@each.sourcePort'),
listenersChanged: function() {
var list = [];
this.get('listenersArray').forEach(function(listener) {
var src = listener.get('sourcePort');
var proto = listener.get('sourceProtocol');
var tgt = listener.get('targetPort');
if ( src && proto )
{
list.pushObject(src + ':' + (tgt ? tgt : src) + (proto === 'http' ? '': '/' + proto ) );
}
});
this.set('model.launchConfig.ports', list.sort().uniq());
}.observes('listenersArray.@each.{sourcePort,sourceProtocol,targetPort,targetProtocol}'),
nameChanged: function() {
this.set('config.name', this.get('balancer.name') + ' config');
}.observes('balancer.name'),
descriptionChanged: function() {
this.set('config.description', this.get('balancer.description'));
}.observes('balancer.description'),
didSave: function() {
var balancer = this.get('model.balancer');
// Set balancer targets
return balancer.waitForNotTransitioning().then(() => {
return balancer.doAction('setservicelinks', {
serviceIds: this.get('targetServiceIds'),
});
});
},
doneSaving: function() {
this.transitionToRoute('environment', this.get('environment.id'));
},
});

View File

@ -0,0 +1,89 @@
import AuthenticatedRouteMixin from 'ui/mixins/authenticated-route';
import Ember from 'ember';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model: function(params/*, transition*/) {
var store = this.get('store');
var dependencies = [
store.findAll('host'),
store.find('environment', params.environmentId).then(function(env) {
return env.importLink('services');
})
];
return Ember.RSVP.all(dependencies, 'Load dependencies').then(function(results) {
var allHosts = results[0];
var environment = results[1];
var launchConfig = store.createRecord({
type: 'container',
});
var lbConfig = store.createRecord({
type: 'loadBalancerConfig',
healthCheck: store.createRecord({
type: 'loadBalancerHealthCheck',
interval: 2000,
responseTimeout: 2000,
healthyThreshold: 2,
unhealthyThreshold: 3,
requestLine: null,
}),
appCookieStickinessPolicy: null,
lbCookieStickinessPolicy: null,
});
return {
isService: true,
allHosts: allHosts,
environment: environment,
balancer: store.createRecord({
type: 'loadBalancerService',
name: '',
description: '',
scale: 1,
environmentId: environment.get('id'),
launchConfig: launchConfig,
loadBalancerConfig: lbConfig,
}),
config: lbConfig,
launchConfig: launchConfig,
appCookie: store.createRecord({
type: 'loadBalancerAppCookieStickinessPolicy',
mode: 'path_parameters',
requestLearn: true,
prefix: false,
timeout: 3600000,
maxLength: 1024,
}),
lbCookie: store.createRecord({
type: 'loadBalancerCookieStickinessPolicy'
}),
};
});
},
setupController: function(controller, model) {
controller.set('model',model);
controller.initFields();
},
resetController: function (controller, isExisting/*, transition*/) {
if (isExisting)
{
controller.set('tab', 'listeners');
controller.set('stickiness', 'none');
}
},
activate: function() {
this.send('setPageLayout', {label: 'Back', backPrevious: true});
},
actions: {
cancel: function() {
this.transitionTo('loadbalancers');
},
}
});

View File

@ -0,0 +1,56 @@
<section class="horizontal-form container-fluid">
<h2>Add Load Balancer</h2>
{{top-errors errors=errors}}
<div class="row form-group">
<div class="col-sm-12 col-md-2 form-label">
<label for="name">Name</label>
</div>
<div class="col-sm-12 col-md-8">
{{input id="name" type="text" value=balancer.name classNames="form-control" placeholder="e.g. Website"}}
</div>
</div>
<div class="row form-group">
<div class="col-sm-12 col-md-2 form-label">
<label for="description">Description</label>
</div>
<div class="col-sm-12 col-md-8">
{{textarea id="description" value=balancer.description classNames="form-control no-resize" rows="5" placeholder="e.g. Balancer for mycompany.com"}}
</div>
</div>
<div class="row form-group">
<div class="col-sm-12 col-md-2 form-label">
<label>Scale</label>
</div>
<div class="col-sm-2 col-md-1">
{{balancer.scale}}
</div>
<div class="col-sm-10 col-md-7">
{{input-slider value=balancer.scale valueMin=1 valueMax=10 scaleMin=0}}
</div>
</div>
{{partial "form-divider"}}
<div class="row">
<div class="col-xs-6 col-md-2 form-label">
<label>Targets</label>
</div>
<div class="col-xs-6 col-md-8">
<button class="btn-circle-plus btn-circle-text" style="margin-right: 20px;" {{action "addTargetService" target="view"}}>Add Service</button>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-8 col-md-offset-2">
{{partial "loadbalancer/edit-targets"}}
</div>
</div>
</section>
{{partial "form-divider"}}
{{partial "loadbalancer/edit-config"}}
{{partial "save-cancel"}}

View File

@ -0,0 +1,37 @@
import Ember from 'ember';
function addAction(action, selector) {
return function() {
this.get('controller').send(action);
Ember.run.next(this, function() {
this.$(selector).last().focus();
});
};
}
export default Ember.View.extend({
actions: {
addTargetService: addAction('addTargetService', '.lb-target'),
addListener: addAction('addListener', '.lb-listener-source-port'),
selectTab: function(name) {
this.set('context.tab',name);
this.$('.tab').removeClass('active');
this.$('.tab[data-section="'+name+'"]').addClass('active');
this.$('.section').addClass('hide');
this.$('.section[data-section="'+name+'"]').removeClass('hide');
}
},
didInsertElement: function() {
$('BODY').addClass('white');
this._super();
this.send('selectTab',this.get('context.tab'));
this.$('INPUT')[0].focus();
},
willDestroyElement: function() {
$('BODY').removeClass('white');
},
});

View File

@ -6,15 +6,27 @@
{{#if tgt.isIp}} {{#if tgt.isIp}}
{{input type="text" class="form-control input-sm lb-target" value=tgt.value placeholder="e.g. 192.0.2.24"}} {{input type="text" class="form-control input-sm lb-target" value=tgt.value placeholder="e.g. 192.0.2.24"}}
{{else}} {{else}}
{{display-name-select {{#if model.isService}}
classNames="form-control input-sm lb-target" {{display-name-select
prompt="Select a container..." classNames="form-control input-sm lb-target"
value=tgt.value prompt="Select a service..."
content=targetChoices value=tgt.value
optionValuePath="content.id" content=targetChoices
optionLabelPath="content.name" optionValuePath="content.id"
optionGroupPath="group" optionLabelPath="content.name"
}} optionGroupPath="group"
}}
{{else}}
{{display-name-select
classNames="form-control input-sm lb-target"
prompt="Select a container..."
value=tgt.value
content=targetChoices
optionValuePath="content.id"
optionLabelPath="content.name"
optionGroupPath="group"
}}
{{/if}}
{{/if}} {{/if}}
</td> </td>
<td width="30" class="text-right"> <td width="30" class="text-right">

View File

@ -6,7 +6,7 @@
<th width="30"></th> <th width="30"></th>
<th>Target</th> <th>Target</th>
<th width="30"></th> <th width="30"></th>
<th>Algorithm</th> <th width="100">Algorithm</th>
<th width="40">&nbsp;</th> <th width="40">&nbsp;</th>
</tr> </tr>
</thead> </thead>
@ -33,35 +33,43 @@
</div> </div>
</td> </td>
<td class="text-center"><i class="ss-right"></i></td> <td class="text-center"><div class="form-control-static input-sm"><i class="ss-right"></i></div></td>
<td> <td>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
{{input type="text" classNames="form-control lb-listener-target-port" min="1" max="65535" step="1" value=listener.targetPort placeholder="e.g. 8080"}} {{input type="text" classNames="form-control lb-listener-target-port" min="1" max="65535" step="1" value=listener.targetPort placeholder="e.g. 8080"}}
<div class="input-group-btn"> {{#if model.isService}}
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false" style="border-left: 0;">/{{listener.targetProtocol}} <span class="caret"></span></button> <span class="input-group-addon">/{{listener.sourceProtocol}}</span>
<ul class="dropdown-menu" role="menu"> {{else}}
<li role="presentation" class="dropdown-header"> <div class="input-group-btn">
Select a protocol: <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false" style="border-left: 0;">/{{listener.targetProtocol}} <span class="caret"></span></button>
</li> <ul class="dropdown-menu" role="menu">
{{#each choice in targetProtocolOptions}} <li role="presentation" class="dropdown-header">
<li {{action "chooseProtocol" listener "targetProtocol" choice}}> Select a protocol:
<a>{{choice}}</a>
</li> </li>
{{/each}} {{#each choice in targetProtocolOptions}}
</ul> <li {{action "chooseProtocol" listener "targetProtocol" choice}}>
</div> <a>{{choice}}</a>
</li>
{{/each}}
</ul>
</div>
{{/if}}
</div> </div>
</td> </td>
<td>&nbsp;</td> <td>&nbsp;</td>
<td> <td>
{{view "select" {{#if model.isService}}
class="form-control input-sm" <div class="form-control-static input-sm">Round Robin</div>
value=listener.algorithm {{else}}
content=algorithmOptions {{view "select"
}} class="form-control input-sm"
value=listener.algorithm
content=algorithmOptions
}}
{{/if}}
</td> </td>
<td class="text-right"> <td class="text-right">
<button {{action "removeListener" listener}} class="btn-circle-x" type="button" tabindex="-1"></button> <button {{action "removeListener" listener}} class="btn-circle-x" type="button" tabindex="-1"></button>

View File

@ -1,6 +1,6 @@
{ {
"name": "ui", "name": "ui",
"version": "0.18.2", "version": "0.19.0",
"private": true, "private": true,
"directories": { "directories": {
"doc": "doc", "doc": "doc",
@ -22,7 +22,7 @@
"devDependencies": { "devDependencies": {
"broccoli-asset-rev": "^2.0.0", "broccoli-asset-rev": "^2.0.0",
"broccoli-sass": "0.6.2", "broccoli-sass": "0.6.2",
"ember-api-store": "1.0.17", "ember-api-store": "^1.0.17",
"ember-browserify": "^0.6.4", "ember-browserify": "^0.6.4",
"ember-cli": "0.2.0", "ember-cli": "0.2.0",
"ember-cli-app-version": "0.3.3", "ember-cli-app-version": "0.3.3",

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="103.7 26 100 83.2" enable-background="new 103.7 26 100 83.2" xml:space="preserve">
<path fill="#3D8ECB" d="M148.5,26.9c4.3-2.3,9.5,0.5,11.7,4.4c1.5,2.4,2,5.3,2.5,8c-1.3,0-2.5,0.1-3.8,0.1c-0.6-2.9-1.3-6.2-3.8-8
c-2-1.7-5.3-1.5-7.1,0.5c-2.2,1.9-2.7,4.9-3.2,7.6c-1.3,0-2.6-0.1-3.8-0.1C141.7,34.4,143.6,29,148.5,26.9z"/>
<path fill="#3D8ECB" d="M135.4,41.6c1.1-0.6,2.7-0.3,3.9-0.3c0.1,9.2,0.1,18.4,0,27.6c-1.3,0-2.6,0-3.9-0.2
c-0.4-5.5-0.1-11-0.1-16.4C135.3,48.7,135,45.1,135.4,41.6z"/>
<path fill="#3D8ECB" d="M141.3,41.3c1.3,0,2.6,0,3.9,0c0.3,9.2,0.3,18.5-0.1,27.7c-1.3-0.1-2.6-0.1-3.9-0.2
C141.3,59.5,141.2,50.4,141.3,41.3z"/>
<path fill="#3D8ECB" d="M147.1,41.3c1.3-0.1,2.5-0.1,3.7-0.2c0.1,2.7-0.1,8.3-0.1,8.3l-3.7-0.1C147,49.2,146.7,44,147.1,41.3z"/>
<path fill="#3D8ECB" d="M152.9,41.3c1.3-0.1,2.7-0.1,4.1-0.1c0.1,2.8-0.1,8.5-0.1,8.5l-4-0.4C152.9,49.2,152.9,44,152.9,41.3z"/>
<path fill="#3D8ECB" d="M158.7,41.3c1.3-0.1,2.5-0.1,3.8-0.1c0,9.2-0.1,27.6-0.1,27.6l-4.1-0.1c0,0,0-15.2-0.1-22.8
C158.4,44.4,158.3,42.8,158.7,41.3z"/>
<path fill="#3D8ECB" d="M164.7,41.3l3.9-0.1v27.6l-3.8-0.1l-0.2-17.1L164.7,41.3z"/>
<path fill="#3D8ECB" d="M124.8,84.2c-0.1-3.8-0.8-8.7-4.6-10.6c-3.6-1.3-7.6-1.2-11.2-0.2c-3,1-4.5,4.3-4.8,7.2
c-0.6,4.4-0.7,9,0.5,13.3c0.7,2.9,3.3,5,6.2,5.2c3.6,0.3,7.9,0.6,10.9-1.8C125.2,93.9,124.8,88.6,124.8,84.2z M119.8,89
c-0.1,2.2-0.8,5.1-3.4,5.6c-2.4,0.3-6.1,0.6-7-2.3c-1-3.7-0.8-7.6-0.2-11.3c0.3-1.5,1.1-3,2.6-3.4c2.3-0.3,5.5-0.8,7,1.5
C120.1,82.2,119.8,85.7,119.8,89z"/>
<path fill="#3D8ECB" d="M127.5,80.2c1.7,0,3.4,0,5,0c0.2,4.8-0.5,9.7,0.3,14.4c1.7,1.9,4.5,0.2,6.4-0.6c0-4.6,0-9.2,0-13.8
c1.7,0,3.4,0,5,0c0,6.3,0,12.7,0,19c-1.3,0-2.7,0-4.1,0c-0.3-0.6,0.3-2.5-0.8-2c-1.2,0.6-5,2.9-7.8,2.1c-2.4-0.5-3.8-2.9-3.9-5.2
C127.3,89.5,127.5,84.8,127.5,80.2z"/>
<path fill="#3D8ECB" d="M164.7,80.2c1.7,0,3.6,0,5.3,0c1,3.9,2,7.9,2.9,11.8c0.2,1.3,1.1,2.4,2,3.4c1.5-5,2.7-10.1,4.1-15.2
c1.7,0,3.5,0,5.2,0c-2,7.7-4.1,15.5-6.1,23.1c-0.6,2.2-2,4-3.2,5.9c-1.2,0-2.4-0.1-3.6-0.1c0.9-3.2,1.7-6.4,2.7-9.6
c-2.2-0.7-4.6-1.7-5.2-4.1C167.3,90.4,166.2,85.3,164.7,80.2z"/>
<path fill="#3D8ECB" d="M150.6,51.1l3.3,0l0.6,8.1l-5.1,0.1L150.6,51.1z"/>
<path fill="#C0C2C4" d="M189.7,85.3c1,0,2,0.1,2.9,0.1c0,0.6,0,2,0,2.6c-1,0-2,0-2.9,0C189.7,87.4,189.7,86,189.7,85.3z"/>
<path fill="#BDBFC1" d="M189.8,89.1c0.9,0,1.8,0,2.7,0c0,3.4,0,6.7,0,10.1c-0.9,0-1.8,0-2.7,0C189.8,95.9,189.8,92.5,189.8,89.1z"/>
<path fill="#3D8ECB" d="M163.1,85c0-2.1-1.3-4.2-3.4-4.8c-4-1.1-8.2-0.3-12.2,0.6c0,1-0.1,2.2-0.1,3.2c3.1-0.1,6.2-0.6,9.3-0.2
c1.5,0.2,1,2.3,1.5,3.4c-3.4,0.3-7.8-1-10.4,1.8c-1.9,3.1-1.7,8.4,2.1,9.9c2.9,1.3,7.1-1,8.3-1.6c1.2-0.6,0.6,1.2,0.8,1.8
c1.4,0,2.7,0,4.1,0C163.1,94.5,163.4,89.7,163.1,85z M158.1,94.1c-2,0.8-4.1,1.3-6.2,1c0-0.6,0-1.3,0.1-2.1c0.2-0.8,0.4-1.3,0.6-1.7
c0.7-0.3,1.6-0.6,2.7-0.8s2-0.3,2.8-0.2C158.1,91.6,158.1,92.9,158.1,94.1z"/>
<path fill="#C0C2C4" d="M203.4,91.9c-0.9-3.1-5-3.6-7.6-2.3c-2.3,1.3-1.9,4.4-1.6,6.6c0.3,3.2,4.3,3.7,6.9,2.9
C204,98.1,204,94.3,203.4,91.9z M200.5,96.7c-0.1,0.1-0.8,0.7-1.7,0.6c-1.2-0.1-1.7-1-1.8-1.1c-0.1-0.6-0.3-1.5-0.2-2.7
c0.1-0.9,0.3-1.6,0.6-2.1c0.2-0.1,2-0.8,2.8,0c0.1,0.1,0.3,0.3,0.4,0.6c0.2,0.5,0.5,1.3,0.5,2.4S200.7,96.3,200.5,96.7z"/>
<path fill="#3D8ECB" d="M146.9,60.5c1.3,0,2.6,0,3.9,0c0.1,2.8,0.1,5.6,0,8.4c-1.3,0-2.5,0-3.8-0.2C146.6,66,146.8,63.2,146.9,60.5z
"/>
<path fill="#3D8ECB" d="M152.9,60.5l4-0.1v8.4l-4-0.1V60.5z"/>
<path fill="#C2C4C6" d="M185.1,96.7c0.9,0,1.7,0,2.7,0c0,0.7,0.1,2,0.1,2.7c-0.9-0.1-1.8-0.1-2.7-0.1
C185.1,98.5,185.1,97.3,185.1,96.7z"/>
<path fill="#3D8ECB" d="M112.4,100.5c0.9,0,1.9,0,2.8,0c1.8,1.3,4.1,1.3,6.2,1.1c0,1.3,0,2.7,0,4.1c-2.2-0.1-4.5,0.1-6.4-1.1
C113.7,103.6,113.1,101.9,112.4,100.5z"/>
<path fill="#FFFFFF" d="M159.3,97.4"/>
<path fill="#FFFFFF" d="M158.8,97.3"/>
<path fill="#FFFFFF" d="M151.3,92.9"/>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB