text: /*
   * Copyright (c) 2016 MariaDB Corporation Ab
   *
   * Use of this software is governed by the Business Source License included
   * in the LICENSE.TXT file and at www.mariadb.com/bsl11.
   *
   * Change Date: 2022-01-01
   *
   * On the date above, in accordance with the Business Source License, use
   * of this software will be governed by version 2 or later of the General
   * Public License.
   */
  require('./common.js')()
  var colors = require('colors/safe')
  
  function equalResources(oldVal, newVal) {
      return _.isEqual(_.get(oldVal, 'attributes.parameters'), _.get(newVal, 'attributes.parameters')) &&
          _.isEqual(_.get(oldVal, 'relationships.servers.data'), _.get(newVal, 'relationships.servers.data')) &&
          _.isEqual(_.get(oldVal, 'relationships.services.data'), _.get(newVal, 'relationships.services.data')) &&
          _.isEqual(_.get(oldVal, 'relationships.monitors.data'), _.get(newVal, 'relationships.monitors.data'))
  }
  
  function sameResource(oldVal, newVal) {
      return oldVal.id == newVal.id
  }
  
  // Return objets that are in <a> but not in <b>
  function getDifference(a, b) {
      return _.differenceWith(a, b, sameResource)
  }
  
  // Return a list of objects that differ from each other
  function getChangedObjects(a, b) {
      var ours = _.intersectionWith(a, b, sameResource)
      var theirs = _.intersectionWith(b, a, sameResource)
      return _.differenceWith(ours, theirs, equalResources)
  }
  
  // Check if the diffs add or delete services
  function haveExtraServices(src, dest, srcHost, destHost) {
      var newObj = getDifference(src.services.data, dest.services.data)
      var oldObj = getDifference(dest.services.data, src.services.data)
  
      if (newObj.length > 0 || oldObj.length > 0) {
          const EOL = require('os').EOL
  
          var srcObj = _.transform(newObj, function(out, i) {
              out.push(i.id)
          })
          var destObj = _.transform(oldObj, function(out, i) {
              out.push(i.id)
          })
          err = ['Cannot diff host `' + srcHost + '` with target `' +
                 destHost + '`: New or deleted services on target host, ' +
                 'both hosts must have the same services.',
                 'Extra services on `' + srcHost + '`:',
                 JSON.stringify(srcObj, null, 4),
                 'Extra services on `' + destHost + '`:',
                 JSON.stringify(destObj, null, 4)
                ]
          return error(err.join(EOL))
      }
  
      return undefined
  }
  
  // Resource collections
  const collections = [
      'servers',
      'monitors',
      'services',
      'users'
  ]
  
  // Individual resources
  const endpoints = [
      'maxscale',
      'maxscale/logs'
  ]
  
  // Calculate a diff between two MaxScale servers
  function getDiffs(a, b) {
      var src = {}
      var dest = {}
      var promises = []
  
      collections.forEach(function(i) {
          promises.push(doAsyncRequest(b, i, function(res) {
              dest[i] = res
          }))
          promises.push(doAsyncRequest(a, i, function(res) {
              src[i] = res
          }))
      })
  
      endpoints.forEach(function(i) {
          promises.push(doAsyncRequest(b, i, function(res) {
              dest[i] = res
          }))
          promises.push(doAsyncRequest(a, i, function(res) {
              src[i] = res
          }))
      })
  
      return Promise.all(promises)
          .then(function() {
              // We can get the listeners only after we've requested the services
              dest.services.data.forEach(function(i) {
                  dest['services/' + i.id + '/listeners'] = { data: i.attributes.listeners }
              })
              src.services.data.forEach(function(i) {
                  src['services/' + i.id + '/listeners'] = { data: i.attributes.listeners }
              })
          })
          .then(function() {
              return Promise.resolve([src, dest])
          })
  }
  
  exports.haveExtraServices = haveExtraServices
  exports.getDifference = getDifference
  exports.getChangedObjects = getChangedObjects
  exports.command = 'cluster <command>'
  exports.desc = 'Cluster objects'
  exports.handler = function() {}
  exports.builder = function(yargs) {
      yargs
          .command('diff <target>', 'Show difference between host servers and <target>.', function(yargs) {
              return yargs.epilog('The list of host servers is controlled with the --hosts option. ' +
                                  'The target server should not be in the host list. Value of <target> ' +
                                  'must be in HOST:PORT format')
                  .usage('Usage: cluster diff <target>')
          }, function(argv) {
  
                       maxctrl(argv, function(host) {
                           return getDiffs(host, argv.target)
                               .then(function(diffs) {
                                   var output = []
                                   var src = diffs[0]
                                   var dest = diffs[1]
  
                                   var err = haveExtraServices(src, dest, host, argv.target)
                                   if (err) {
                                       return err
                                   }
  
                                   _.uniq(_.concat(Object.keys(src), Object.keys(dest))).forEach(function(i) {
                                       var newObj = getDifference(src[i].data, dest[i].data)
                                       var oldObj = getDifference(dest[i].data, src[i].data)
                                       var changed = getChangedObjects(src[i].data, dest[i].data)
  
                                       if (newObj.length) {
                                           output.push("New:", i)
                                           output.push(colors.green(JSON.stringify(newObj, null, 4)))
                                       }
                                       if (oldObj.length) {
                                           output.push("Deleted:", i)
                                           output.push(colors.red(JSON.stringify(oldObj, null, 4)))
                                       }
                                       if (changed.length) {
                                           output.push("Changed:", i)
                                           output.push(colors.yellow(JSON.stringify(changed, null, 4)))
                                       }
                                   })
                                   endpoints.forEach(function(i) {
                                       // Treating the resource endpoints as arrays allows the same functions to be used
                                       // to compare individual resources and resource collections
                                       var changed = getChangedObjects([src[i].data], [dest[i].data])
                                       if (changed.length) {
                                           output.push("Changed:", i)
                                           output.push(colors.yellow(JSON.stringify(changed, null, 4)))
                                       }
                                   })
  
                                   return output.join(require('os').EOL)
                               })
                       })
                   })
          .command('sync <target>', 'Synchronize the cluster with target MaxScale server.', function(yargs) {
              return yargs.epilog('This command will alter all MaxScale instances given in the --hosts ' +
                                  'option to represent the <target> MaxScale. If the synchronization of ' +
                                  'a MaxScale instance fails, it will be disabled by executing the `stop maxscale` ' +
                                  'command on that instance. Synchronization can be attempted again if a previous ' +
                                  'attempt failed due to a network failure or some other ephemeral error. Any other ' +
                                  'errors require manual synchronization of the MaxScale configuration files and a ' +
                                  'restart of the failed Maxscale.')
                  .usage('Usage: cluster sync <target>')
          }, function(argv) {
              maxctrl(argv, function(host) {
                  return getDiffs(argv.target, host)
                      .then(function(diffs) {
                          var promises = []
                          var src = diffs[0]
                          var dest = diffs[1]
  
                          var err = haveExtraServices(src, dest, host, argv.target)
                          if (err) {
                              return err
                          }
  
                          // Delete old servers
                          getDifference(dest.servers.data, src.servers.data).forEach(function(i) {
                              // First unlink the servers from all services and monitors
                              promises.push(
                                  doAsyncRequest(host, 'servers/' + i.id, null, {method: 'PATCH', body: _.set({}, 'data.relationships', {})})
                                      .then(function() {
                                          return doAsyncRequest(host, 'servers/' + i.id, null, {method: 'DELETE'})
                                      }))
                          })
  
                          // Add new servers
                          getDifference(src.servers.data, dest.servers.data).forEach(function(i) {
                              // Create the servers without relationships, those are generated when services and
                              // monitors are updated
                              var newserv = {
                                  data: {
                                      id: i.id,
                                      type: i.type,
                                      attributes: {
                                          parameters: i.attributes.parameters
                                      }
                                  }
                              }
                              promises.push(doAsyncRequest(host, 'servers', null, {method: 'POST', body: newserv}))
                          })
                          return Promise.all(promises)
                              .then(function() {
                                  var promises = []
                                  // Delete old monitors
                                  getDifference(dest.monitors.data, src.monitors.data).forEach(function(i) {
                                      promises.push(
                                          doAsyncRequest(host, 'monitors/' + i.id, null, {
                                              method: 'PATCH', body: _.set({}, 'data.relationships', {})
                                          })
                                              .then(function() {
                                                  return doAsyncRequest(host, 'monitors/' + i.id, null, {method: 'DELETE'})
                                              }))
                                  })
                                  return Promise.all(promises)
                              })
                              .then(function() {
                                  var promises = []
                                  // Add new monitors
                                  getDifference(src.monitors.data, dest.monitors.data).forEach(function(i) {
                                      promises.push(doAsyncRequest(host, 'monitors', null, {method: 'POST', body: {data: i}}))
                                  })
                                  return Promise.all(promises)
                              })
                              .then(function() {
                                  // Add new and delete old listeners
                                  var promises = []
                                  var all_keys = _.concat(Object.keys(src), Object.keys(dest))
                                  var unwanted_keys = _.concat(collections, endpoints)
                                  var relevant_keys = _.uniq(_.difference(all_keys, unwanted_keys))
  
                                  relevant_keys.forEach(function(i) {
                                      getDifference(dest[i].data, src[i].data).forEach(function(j) {
                                          promises.push(doAsyncRequest(host, i + '/' + j.id, null, {method: 'DELETE'}))
                                      })
                                      getDifference(src[i].data, dest[i].data).forEach(function(j) {
                                          promises.push(doAsyncRequest(host, i, null, {method: 'POST', body: {data: j}}))
                                      })
                                  })
  
                                  return Promise.all(promises)
                              })
                              .then(function() {
                                  var promises = []
                                  // PATCH all remaining resource collections in src from dest apart from the
                                  // user resource as it requires passwords to be entered
                                  _.difference(collections, ['users']).forEach(function(i) {
                                      src[i].data.forEach(function(j) {
                                          promises.push(doAsyncRequest(host, i + '/' + j.id, null, {method: 'PATCH', body: {data: j}}))
                                      })
                                  })
                                  // Do the same for individual resources
                                  endpoints.forEach(function(i) {
                                      promises.push(doAsyncRequest(host, i, null, {method: 'PATCH', body: dest[i]}))
                                  })
                                  return Promise.all(promises)
                              })
                      })
              })
          })
          .usage('Usage: cluster <command>')
          .help()
          .command('*', 'the default command', {}, function(argv) {
              maxctrl(argv, function() {
                  return error('Unknown command. See output of `help cluster` for a list of commands.')
              })
          })
  }
  