Forráskód Böngészése

AMBARI-15780. Kerberos wizard: Integrate Configure Identities page with stack advisor recommendations (alexantonenko)

Alex Antonenko 9 éve
szülő
commit
6de04a33c6

+ 2 - 1
ambari-web/app/config.js

@@ -81,7 +81,8 @@ App.supports = {
   logSearch: false,
   redhatSatellite: false,
   enableIpa: false,
-  addingNewRepository: false
+  addingNewRepository: false,
+  kerberosStackAdvisor: true
 };
 
 if (App.enableExperimental) {

+ 10 - 0
ambari-web/app/controllers/main/admin/kerberos.js

@@ -595,6 +595,16 @@ App.MainAdminKerberosController = App.KerberosWizardStep4Controller.extend({
 
   showManageKDCCredentialsPopup: function() {
     return App.showManageCredentialsPopup();
+  },
+  
+  loadStep: function() {
+    var self = this;
+    this.clearStep();
+    this.getDescriptor().then(function (properties) {
+      self.setStepConfigs(self.createServicesStackDescriptorConfigs(properties));
+    }).always(function() {
+      self.set('isRecommendedLoaded', true);
+    });
   }
 
 });

+ 329 - 21
ambari-web/app/controllers/main/admin/kerberos/step4_controller.js

@@ -23,6 +23,9 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
   name: 'kerberosWizardStep4Controller',
   isWithinAddService: Em.computed.equal('wizardController.name', 'addServiceController'),
 
+  // stores configurations loaded by ConfigurationsController.getConfigsByTags
+  servicesConfigurations: null,
+
   clearStep: function() {
     this.set('isRecommendedLoaded', false);
     this.set('selectedService', null);
@@ -30,15 +33,28 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
   },
 
   loadStep: function() {
+    var self, stored;
     if (this.get('wizardController.skipConfigureIdentitiesStep')) {
       App.router.send('next');
       return;
     }
-    var self = this;
+    self = this;
     this.clearStep();
-    this.getDescriptor().then(function (properties) {
-      self.setStepConfigs(self.createServicesStackDescriptorConfigs(properties));
-    }).always(function() {
+    stored = this.get('wizardController').loadCachedStepConfigValues(this) || [];
+    this.getDescriptor().then(function (kerberosDescriptor) {
+      var stepConfigs = self.setStepConfigs(self.createServicesStackDescriptorConfigs(kerberosDescriptor));
+      self.set('stepConfigs', stepConfigs);
+      // when configurations were stored no need to apply recommendations again
+      if (App.get('supports.kerberosStackAdvisor') && !stored.length) {
+        self.bootstrapRecommendationPayload(kerberosDescriptor).then(function(recommendations) {
+          self.loadServerSideConfigsRecommendations(recommendations).done(function() {
+            self.applyServiceConfigs(stepConfigs);
+          });
+        });
+      } else {
+        self.applyServiceConfigs(stepConfigs);
+      }
+    }, function() {
       self.set('isRecommendedLoaded', true);
     });
   },
@@ -100,6 +116,7 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
       serviceName: 'KERBEROS_GENERAL',
       configCategories: categoryForGeneralConfigs,
       configs: generalConfigs,
+      configGroups: [],
       showConfig: true
     }),
       App.ServiceConfig.create({
@@ -108,6 +125,7 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
         serviceName: 'KERBEROS_ADVANCED',
         configCategories: categoryForAdvancedConfigs,
         configs: advancedConfigs,
+        configGroups: [],
         showConfig: true
       })
     ];
@@ -119,7 +137,7 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
    */
   createCategoryForServices: function() {
     var services = [];
-    if (this.get('wizardController.name') == 'addServiceController') {
+    if (this.get('wizardController.name') === 'addServiceController') {
       services = App.StackService.find().filter(function(item) {
         return item.get('isInstalled') || item.get('isSelected');
       });
@@ -135,13 +153,13 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
    * Prepare step configs using stack descriptor properties.
    *
    * @param {App.ServiceConfigProperty[]} configs
-   * @param {App.ServiceConfigProperty[]} stackConfigs
+   * @param {App.ServiceConfigProperty[]} stackConfigs converted kerberos descriptor
    */
   setStepConfigs: function(configs, stackConfigs) {
     var configProperties = this.prepareConfigProperties(configs),
       stackConfigProperties = stackConfigs ? this.prepareConfigProperties(stackConfigs) : [],
       alterProperties = ['value','initialValue', 'defaultValue'];
-    if (this.get('wizardController.name') == 'addServiceController') {
+    if (this.get('wizardController.name') === 'addServiceController') {
       // config properties for installed services should be disabled on Add Service Wizard
       configProperties.forEach(function(item) {
         if (this.get('installedServiceNames').contains(item.get('serviceName')) || item.get('serviceName') == 'Cluster') {
@@ -159,7 +177,7 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
       stackConfigProperties.forEach(function(_stackConfigProperty){
         var isPropertyInClusterDescriptor = configProperties.filterProperty('filename', _stackConfigProperty.get('filename')).someProperty('name', _stackConfigProperty.get('name'));
         if (!isPropertyInClusterDescriptor) {
-          if (this.get('installedServiceNames').contains(_stackConfigProperty.get('serviceName')) || _stackConfigProperty.get('serviceName') == 'Cluster') {
+          if (this.get('installedServiceNames').contains(_stackConfigProperty.get('serviceName')) || _stackConfigProperty.get('serviceName') === 'Cluster') {
             _stackConfigProperty.set('isEditable', false);
           }
           configProperties.pushObject(_stackConfigProperty);
@@ -167,8 +185,9 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
       }, this);
     }
     configProperties = App.config.sortConfigs(configProperties);
-    this.get('stepConfigs').pushObjects(this.createServiceConfig(configProperties));
-    this.set('selectedService', this.get('stepConfigs')[0]);
+    var stepConfigs = this.createServiceConfig(configProperties);
+    this.set('selectedService', stepConfigs[0]);
+    return stepConfigs;
   },
 
   /**
@@ -189,6 +208,7 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
     var installedServiceNames = ['Cluster'].concat(App.Service.find().mapProperty('serviceName'));
     var configProperties = configs.slice(0);
     var siteProperties = App.configsCollection.getAll();
+    var realmValue;
     // override stored values
     App.config.mergeStoredValue(configProperties, this.get('wizardController').loadCachedStepConfigValues(this));
 
@@ -199,8 +219,8 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
     configProperties = configProperties.filter(function(item) {
       return installedServiceNames.contains(item.get('serviceName'));
     });
-    if (this.get('wizardController.name') != 'addServiceController') {
-      var realmValue = storedServiceConfigs.findProperty('name', 'realm').value;
+    if (this.get('wizardController.name') !== 'addServiceController') {
+      realmValue = storedServiceConfigs.findProperty('name', 'realm').value;
       configProperties.findProperty('name', 'realm').set('value', realmValue);
       configProperties.findProperty('name', 'realm').set('savedValue', realmValue);
       configProperties.findProperty('name', 'realm').set('recommendedValue', realmValue);
@@ -216,7 +236,7 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
         property.set('value', observedValue);
         property.set('recommendedValue', observedValue);
       }
-      if (property.get('serviceName') == 'Cluster') {
+      if (property.get('serviceName') === 'Cluster') {
         property.set('category', 'Global');
       }
       else {
@@ -246,17 +266,21 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
    * Function to override kerberos descriptor's property values
    */
   tweakConfigProperty: function(config) {
+    var defaultHiveMsPort = "9083",
+        hiveMSHosts,
+        port,
+        hiveMSHostNames,
+        configValue;
     if (config.name === 'templeton.hive.properties') {
-      var defaultHiveMsPort = "9083";
-      var hiveMSHosts = App.HostComponent.find().filterProperty('componentName', 'HIVE_METASTORE');
+      hiveMSHosts = App.HostComponent.find().filterProperty('componentName', 'HIVE_METASTORE');
       if (hiveMSHosts.length > 1) {
-        var hiveMSHostNames = hiveMSHosts.mapProperty('hostName');
-        var port = config.value.match(/:[0-9]{2,4}/);
+        hiveMSHostNames = hiveMSHosts.mapProperty('hostName');
+        port = config.value.match(/:[0-9]{2,4}/);
         port = port ? port[0].slice(1) : defaultHiveMsPort;
         for (var i = 0; i < hiveMSHostNames.length; i++) {
           hiveMSHostNames[i] = "thrift://" + hiveMSHostNames[i] + ":" + port;
         }
-        var configValue =  config.value.replace(/thrift.+[0-9]{2,},/i, hiveMSHostNames.join('\\,') + ",");
+        configValue = config.value.replace(/thrift.+[0-9]{2,},/i, hiveMSHostNames.join('\\,') + ",");
         config.set('value', configValue);
         config.set('recommendedValue', configValue);
       }
@@ -265,14 +289,14 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
 
   /**
    * Sync up values between inherited property and its reference.
-   * 
+   *
    * @param {App.ServiceConfigProperty} configProperty
    */
   spnegoPropertiesObserver: function(configProperty) {
     var self = this;
-    var stepConfig =  this.get('stepConfigs').findProperty('name', 'ADVANCED');
+    var stepConfig = this.get('stepConfigs').findProperty('name', 'ADVANCED');
     stepConfig.get('configs').forEach(function(config) {
-      if (config.get('observesValueFrom') == configProperty.get('name')) {
+      if (config.get('observesValueFrom') === configProperty.get('name')) {
         Em.run.once(self, function() {
           config.set('value', configProperty.get('value'));
           config.set('recommendedValue', configProperty.get('value'));
@@ -294,5 +318,289 @@ App.KerberosWizardStep4Controller = App.WizardStep7Controller.extend(App.AddSecu
     });
     this.updateKerberosDescriptor(kerberosDescriptor, configs);
     App.get('router.kerberosWizardController').saveKerberosDescriptorConfigs(kerberosDescriptor);
+  },
+
+  /**
+   * Add/update property in `properties` object for each config type with
+   * associated kerberos descriptor config value.
+   *
+   * @private
+   * @param {object[]} configurations
+   * @param {App.ServiceConfigProperty[]} kerberosDescriptor
+   * @returns {object[]}
+   */
+  mergeDescriptorToConfigurations: function(configurations, kerberosDescriptor) {
+    return configurations.map(function(configType) {
+      var properties = $.extend({}, configType.properties);
+      var filteredDescriptor = kerberosDescriptor.filterProperty('filename', configType.type);
+      if (filteredDescriptor.length) {
+        filteredDescriptor.forEach(function(descriptorConfig) {
+          var configName = Em.get(descriptorConfig, 'name');
+          properties[configName] = Em.get(descriptorConfig, 'value');
+        });
+      }
+      return {
+        type: configType.type,
+        version: configType.version,
+        tag: configType.tag,
+        properties: properties
+      };
+    });
+  },
+
+  loadServerSideConfigsRecommendations: function(recommendations) {
+    return App.ajax.send({
+      'name': 'config.recommendations',
+      'sender': this,
+      'data': {
+        stackVersionUrl: App.get('stackVersionURL'),
+        dataToSend: {
+          recommend: 'configurations',
+          hosts: this.get('hostNames'),
+          services: this.get('serviceNames'),
+          recommendations: recommendations
+        }
+      },
+      'success': 'loadRecommendationsSuccess',
+      'error': 'loadRecommendationsError'
+    });
+  },
+
+  applyServiceConfigs: function(stepConfigs) {
+    this.set('isRecommendedLoaded', true);
+    this.set('selectedService', stepConfigs[0]);
+  },
+
+  /**
+   * Callback executed when all configs specified by tags are loaded.
+   * Here we handle configurations for instlled services and Kerberos.
+   * Gather needed info for recommendation request such as configurations object.
+   *
+   * @override
+   */
+  getConfigTagsSuccess: function(data) {
+    // here we get all installed services including KERBEROS
+    var serviceNames = App.Service.find().mapProperty('serviceName').concat(['KERBEROS']).uniq();
+    // collect all config types for selected services
+    var installedServiceSites = Array.prototype.concat.apply([], App.config.get('preDefinedServiceConfigs').filter(function(serviceConfig) {
+      return serviceNames.contains(Em.get(serviceConfig, 'serviceName'));
+    }).map(function (service) {
+      // when service have no configs return <code>null</code> instead return config types
+      if (!service.get('configTypes')) return null;
+      return Object.keys(service.get('configTypes'));
+    }, this).compact()).uniq(); // cleanup <code>null</code>
+
+    // take all configs for selected services by config types recieved from API response
+    var serviceConfigTags = Em.keys(data.Clusters.desired_configs).reduce(function(tags, site) {
+      if (data.Clusters.desired_configs.hasOwnProperty(site)) {
+        // push cluster-env.xml also since it not associated with any service but need to further processing
+        if (installedServiceSites.contains(site) || site === 'cluster-env') {
+          tags.push({
+            siteName: site,
+            tagName: data.Clusters.desired_configs[site].tag,
+            newTagName: null
+          });
+        }
+      }
+      return tags;
+    }, []);
+    // store configurations
+    this.set('serviceConfigTags', serviceConfigTags);
+    this.set('isAppliedConfigLoaded', true);
+  },
+
+  /**
+   * Prepare all necessary data for recommendations payload.
+   *
+   * #mutates initialConfigValues
+   * @returns {$.Deferred.promise()}
+   */
+  bootstrapRecommendationPayload: function(kerberosDescriptor) {
+    var dfd = $.Deferred();
+    var self = this;
+    this.getServicesConfigurations().then(function(configurations) {
+      var recommendations = self.getBlueprintPayloadObject(configurations, kerberosDescriptor);
+      self.set('servicesConfigurations', configurations);
+      self.set('initialConfigValues', recommendations.blueprint.configurations);
+      dfd.resolve(recommendations);
+    });
+    return dfd.promise();
+  },
+
+  getServicesConfigurations: function() {
+    var dfd = $.Deferred();
+    var self = this;
+    var configs, servicesConfigurations;
+    if (this.get('isWithinAddService')) {
+      configs = this.get('wizardController').getDBProperty('serviceConfigProperties');
+      servicesConfigurations = configs.reduce(function(configTags, property) {
+        var fileName = App.config.getConfigTagFromFileName(property.filename),
+            configType;
+        if (!configTags.someProperty('type', fileName)) {
+          configTags.push({
+            type: fileName,
+            properties: {}
+          });
+        }
+        configType = configTags.findProperty('type', fileName);
+        configType.properties[property.name] = property.value;
+        return configTags;
+      }, []);
+      dfd.resolve(servicesConfigurations);
+    } else {
+      this.getConfigTags().then(function() {
+        App.router.get('configurationController').getConfigsByTags(self.get('serviceConfigTags')).done(function (configurations) {
+          dfd.resolve(configurations);
+        });
+      });
+    }
+
+    return dfd.promise();
+  },
+
+  /**
+   * Returns payload for recommendations request.
+   * Takes services' configurations and merge them with kerberos descriptor properties.
+   *
+   * @param {object[]} configurations services' configurations fetched from API
+   * @param {App.ServiceConfigProperty[]} kerberosDescriptor descriptor configs
+   * @returns {object} payload for recommendations request
+   */
+  getBlueprintPayloadObject: function(configurations, kerberosDescriptor) {
+    var recommendations = this.get('hostGroups');
+    var mergedConfigurations = this.mergeDescriptorToConfigurations(configurations, this.createServicesStackDescriptorConfigs(kerberosDescriptor));
+    recommendations.blueprint.configurations = mergedConfigurations.reduce(function(p, c) {
+      p[c.type] = {};
+      p[c.type].properties = c.properties;
+      return p;
+    }, {});
+
+    return recommendations;
+  },
+
+  /**
+   * @override
+   */
+  _saveRecommendedValues: function(data) {
+    var recommendedConfigurations = Em.getWithDefault(data, 'resources.0.recommendations.blueprint.configurations', {});
+    var allConfigs = Array.prototype.concat.apply([], this.get('stepConfigs').mapProperty('configs'));
+    var self = this;
+    // iterate by each config file name e.g. hdfs-site
+    var groupedProperties = this.groupRecommendationProperties(recommendedConfigurations, this.get('servicesConfigurations'), allConfigs);
+    var newProperties = [];
+    Em.keys(groupedProperties.add).forEach(function(fileName) {
+      var serviceName = self.getServiceByFilename(fileName);
+      Em.keys(groupedProperties.add[fileName]).forEach(function(propertyName) {
+        var property = self._createNewProperty(propertyName, fileName, serviceName, groupedProperties.add[fileName][propertyName]);
+        property.set('category', serviceName);
+        property.set('isOverridable', false);
+        property.set('supportsFinal', false);
+        property.set('isUserProperty', false);
+        property.set('filename', fileName);
+        newProperties.push(property);
+      });
+    });
+    Array.prototype.push.apply(self.getServicesConfigObject().get('configs'), newProperties);
+    Em.keys(groupedProperties.update).forEach(function(fileName) {
+      Em.keys(groupedProperties.update[fileName]).forEach(function(propertyName) {
+        var configProperty = allConfigs.findProperty('name', propertyName);
+        if (configProperty) {
+          self._updateConfigByRecommendation(configProperty, groupedProperties.update[fileName][propertyName], true, false);
+        }
+      });
+    });
+    Em.keys(groupedProperties.delete).forEach(function(fileName) {
+      Em.keys(groupedProperties.delete[fileName]).forEach(function(propertyName) {
+        var servicesConfigs = self.getServicesConfigObject().get('configs');
+        servicesConfigs.removeObject(servicesConfigs.filterProperty('filename', fileName).findProperty('name', propertyName));
+      });
+    });
+  },
+
+  /**
+   * Returns category where services' configuration located.
+   *
+   * @returns {App.ServiceConfig}
+   */
+  getServicesConfigObject: function() {
+    return this.get('stepConfigs').findProperty('name', 'ADVANCED');
+  },
+
+  /**
+   * Returns map with appropriate action and properties to process with.
+   * Key is an action e.g. `add`, `update`, `delete` and value is  an object `fileName` -> `propertyName`: `propertyValue`.
+   *
+   * @param {object} recommendedConfigurations
+   * @param {object[]} servicesConfigurations services' configurations fetched from API
+   * @param {App.ServiceConfigProperty[]} allConfigs all current configurations stored in controller, basically kerberos descriptor
+   * @returns {object}
+   */
+  groupRecommendationProperties: function(recommendedConfigurations, servicesConfigurations, allConfigs) {
+    var resultMap = {
+      update: {},
+      add: {},
+      delete: {}
+    };
+
+    /**
+     * Adds property to associated group `add`,`delete`,`update`.
+     *
+     * @param {object} propertyMap <code>resultMap</code> object
+     * @param {string} name property name
+     * @param {string} propertyValue property value
+     * @param {string} fileName property file name
+     * @return {object} <code>resultMap</code>
+     * @param {string} group, `add`,`delete`,`update`
+     */
+    var addProperty = function(propertyMap, name, propertyValue, fileName, group) {
+      var ret = $.extend(true, {}, propertyMap);
+      if (ret.hasOwnProperty(group)) {
+        if (!ret[group].hasOwnProperty(fileName)) {
+          ret[group][fileName] = {};
+        }
+        ret[group][fileName][name] = propertyValue;
+      }
+      return ret;
+    };
+
+    return Em.keys(recommendedConfigurations || {}).reduce(function(acc, fileName) {
+      var propertyMap = acc;
+      var recommendedProperties = Em.getWithDefault(recommendedConfigurations, fileName + '.properties', {});
+      var recommendedAttributes = Em.getWithDefault(recommendedConfigurations, fileName + '.property_attributes', {});
+      // check for properties that should be delted
+      Em.keys(recommendedAttributes).forEach(function(propertyName) {
+        var attribute = recommendedAttributes[propertyName];
+        // delete properties which are present in kerberos descriptor
+        if (attribute.hasOwnProperty('delete') && allConfigs.filterProperty('filename', fileName).someProperty('name', propertyName)) {
+          propertyMap = addProperty(propertyMap, propertyName, '', fileName, 'delete');
+        }
+      });
+
+      return Em.keys(recommendedProperties).reduce(function(a, propertyName) {
+        var propertyValue = recommendedProperties[propertyName];
+        // check if property exist in saved configurations on server
+        var isExist = Em.getWithDefault(servicesConfigurations.findProperty('type', fileName) || {}, 'properties', {}).hasOwnProperty(propertyName);
+        if (!isExist) {
+          return addProperty(a, propertyName, propertyValue, fileName, 'add');
+        }
+        // when property exist check that it present in current step configs (kerberos descriptor)
+        // and add it as property to `update`
+        if (allConfigs.filterProperty('filename', fileName).someProperty('name', propertyName)) {
+          return addProperty(a, propertyName, propertyValue, fileName, 'update');
+        }
+        return a;
+      }, propertyMap);
+    }, resultMap);
+  },
+
+  getServiceByFilename: function(fileName) {
+    // core-site properties goes to HDFS
+    if (fileName === 'core-site' && App.Service.find().someProperty('serviceName', 'HDFS')) {
+      return 'HDFS';
+    }
+    var associatedService = App.StackService.find().filter(function(service) {
+      return Em.keys(service.get('configTypes')).contains(fileName);
+    })[0];
+    return associatedService ? associatedService.get('serviceName') : '';
   }
 });

+ 13 - 6
ambari-web/app/mixins/wizard/addSecurityConfigs.js

@@ -17,7 +17,6 @@
  */
 
 var App = require('app');
-var objectUtils = require('utils/object_utils');
 
 /**
  * Mixin for loading and setting secure configs
@@ -167,7 +166,12 @@ App.AddSecurityConfigs = Em.Mixin.create({
         configObject.referenceProperty = name.substring(1) + ':' + item;
         configObject.isEditable = false;
       }
-      configObject.defaultValue = configObject.savedValue = configObject.value = itemValue;
+      Em.setProperties(configObject, {
+        recommendedValue: itemValue,
+        initialValue: itemValue,
+        defaultValue: itemValue,
+        value: itemValue
+      });
       configObject.filename = prop.configuration ? prop.configuration.split('/')[0] : 'cluster-env';
       configObject.name = prop.configuration ? prop.configuration.split('/')[1] : name + '_' + item;
       predefinedProperty = self.get('kerberosDescriptorProperties').findProperty('name', configObject.name);
@@ -207,17 +211,20 @@ App.AddSecurityConfigs = Em.Mixin.create({
 
     for (var propertyName in kerberosProperties) {
       var predefinedProperty = this.get('kerberosDescriptorProperties').findProperty('name', propertyName);
+      var value = kerberosProperties[propertyName];
+      var isRequired = propertyName == 'additional_realms' ? false : value !== "";
       var propertyObject = {
         name: propertyName,
-        value: kerberosProperties[propertyName],
-        defaultValue: kerberosProperties[propertyName],
-        savedValue: kerberosProperties[propertyName],
+        value: value,
+        defaultValue: value,
+        recommendedValue: value,
+        initialValue: value,
         serviceName: serviceName,
         filename: filename,
         displayName: serviceName == "Cluster" ? App.format.normalizeName(propertyName) : propertyName,
         isOverridable: false,
         isEditable: propertyName != 'realm',
-        isRequired: propertyName != 'additional_realms',
+        isRequired: isRequired,
         isSecureConfig: true,
         placeholderText: predefinedProperty && !Em.isNone(predefinedProperty.index) ? predefinedProperty.placeholderText : '',
         index: predefinedProperty && !Em.isNone(predefinedProperty.index) ? predefinedProperty.index : Infinity

+ 199 - 3
ambari-web/test/controllers/main/admin/kerberos/step4_controller_test.js

@@ -213,8 +213,8 @@ describe('App.KerberosWizardStep4Controller', function() {
           })
         });
         sinon.stub(App.router, 'get').withArgs('mainAdminKerberosController.isManualKerberos').returns(false);
-        controller.setStepConfigs(properties);
-        res = controller.get('stepConfigs')[0].get('configs').concat(controller.get('stepConfigs')[1].get('configs'));
+        var stepConfigs = controller.setStepConfigs(properties);
+        res = stepConfigs[0].get('configs').concat(stepConfigs[1].get('configs'));
       });
 
       Em.A([
@@ -290,7 +290,7 @@ describe('App.KerberosWizardStep4Controller', function() {
         this.wizardController = App.AddServiceController.create({});
         controller.set('wizardController', this.wizardController);
         sinon.stub(controller, 'clearStep').returns(true);
-
+        sinon.stub(controller, 'getDescriptor').returns({ then: function() { return { always: function() {}}}});
         sinon.stub(controller, 'setStepConfigs').returns(true);
         sinon.stub(App.router, 'send').withArgs('next');
       });
@@ -341,4 +341,200 @@ describe('App.KerberosWizardStep4Controller', function() {
       });
     });
   });
+
+  describe('#mergeDescriptorToConfigurations', function() {
+    var genAppConfigProperty = function(name, fileName, value) {
+      return App.ServiceConfigProperty.create({
+        name: name,
+        filename: fileName,
+        value: value
+      });
+    };
+
+    var genPropertyCollection = function(configsList) {
+      return configsList.map(function(i) {
+        return genAppConfigProperty.apply(undefined, i);
+      });
+    };
+
+    var genConfigType = function(fileName, properties) {
+      var configTypeObj = {};
+      configTypeObj.type = fileName;
+      configTypeObj.properties = properties.reduce(function(p,c) {
+        p[c[0]] = c[1];
+        return p;
+      }, {});
+      return configTypeObj;
+    };
+
+    var genConfigTypeCollection = function(coll) {
+      return coll.map(function(i) {
+        return genConfigType(i[0], i[1]);
+      });
+    };
+
+    var cases = [
+      {
+        kerberosDescriptor: genPropertyCollection([]),
+        configurations: [],
+        e: [],
+        m: 'should return empty array'
+      },
+      {
+        kerberosDescriptor: genPropertyCollection([
+          ['hadoop.proxy.group', 'hadoop-env', 'val1']
+        ]),
+        configurations: genConfigTypeCollection([
+          ['hadoop-env', [
+           ['hadoop.proxy.group', 'change_me'],
+           ['hadoop.proxy', 'val2']
+          ]],
+          ['core-site', [
+            ['hadoop.proxyuser.hcat.groups', '*']
+          ]]
+        ]),
+        e: [
+          {
+            type: 'hadoop-env',
+            properties: {
+              'hadoop.proxy.group': 'val1',
+              'hadoop.proxy': 'val2'
+            }
+          },
+          {
+            type: 'core-site',
+            properties: {
+              'hadoop.proxyuser.hcat.groups': '*'
+            }
+          }
+        ],
+        m: 'should change value of `hadoop.proxy.group`, rest object should not be changed.'
+      },
+      {
+        kerberosDescriptor: genPropertyCollection([
+          ['hadoop.proxy.group', 'hadoop-env', 'val1'],
+          ['new_site_prop', 'core-site', 'new_val']
+        ]),
+        configurations: genConfigTypeCollection([
+          ['hadoop-env', [
+            ['hadoop.proxy.group', 'val1'],
+            ['hadoop.proxy', 'val2']
+          ]],
+          ['core-site', [
+            ['hadoop.proxyuser.hcat.groups', '*']
+          ]]
+        ]),
+        e: [
+          {
+            type: 'hadoop-env',
+            properties: {
+              'hadoop.proxy.group': 'val1',
+              'hadoop.proxy': 'val2'
+            }
+          },
+          {
+            type: 'core-site',
+            properties: {
+              'hadoop.proxyuser.hcat.groups': '*',
+              'new_site_prop': 'new_val'
+            }
+          }
+        ],
+        m: 'should add property `new_site_prop` value to `core-site` file type, rest object should not be changed.'
+      }
+    ];
+
+    cases.forEach(function(test) {
+      it(test.m, function() {
+        var toObj = function(res) {
+          return JSON.parse(JSON.stringify(res));
+        };
+        expect(toObj(c.mergeDescriptorToConfigurations(test.configurations, test.kerberosDescriptor))).to.be.eql(test.e);
+      });
+    });
+  });
+
+  describe('#groupRecommendationProperties', function() {
+    var cases, controller;
+    beforeEach(function() {
+      controller = App.KerberosWizardStep4Controller.create({});
+    });
+
+    afterEach(function() {
+      controller.destroy();
+      controller = null;
+    });
+
+    cases = [
+      {
+        recommendedConfigurations: {},
+        servicesConfigurations: [],
+        allConfigs: [],
+        m: 'empty objects should not fail the code',
+        e: {
+          add: {},
+          update: {},
+          delete: {}
+        }
+      },
+      {
+        recommendedConfigurations: {
+          'some-site': {
+            properties: {
+              // property absent from servicesConfigurations and allConfigs
+              // should be added
+              'new_prop1': 'val1',
+              // property present in servicesConfigurations but absent in  allConfigs
+              // should be skipped
+              'new_prop2': 'val2',
+              'existing-prop': 'updated_val2'
+            },
+            property_attributes: {
+              'delete_prop1': {
+                'delete': true
+              }
+            }
+          }
+        },
+        servicesConfigurations: [
+          {
+            type: 'some-site',
+            properties: {
+              'existing-prop': 'val2',
+              'new_prop2': 'val2'
+            }
+          }
+        ],
+        allConfigs: [
+          Em.Object.create({ name: 'existing-prop', value: 'val3', filename: 'some-site'}),
+          Em.Object.create({ name: 'delete_prop1', value: 'val', filename: 'some-site'})
+        ],
+        m: 'should add "new_prop1", remove "delete_prop1", skip adding "new_prop2" and update value for "existing-prop"',
+        e: {
+          update: {
+            'some-site': {
+              'existing-prop': 'updated_val2'
+            }
+          },
+          add: {
+            'some-site': {
+              'new_prop1': 'val1'
+            }
+          },
+          delete: {
+            'some-site': {
+              'delete_prop1': ''
+            }
+          }
+        }
+      }
+    ];
+
+    cases.forEach(function(test) {
+      it(test.m, function() {
+        expect(controller.groupRecommendationProperties(test.recommendedConfigurations, test.servicesConfigurations, test.allConfigs))
+          .to.be.eql(test.e);
+      });
+    });
+  });
 });