# Code coverage report for lib/consul/index.js

## Statements: 100% (81 / 81) Branches: 100% (39 / 39) Functions: 100% (20 / 20) Lines: 100% (75 / 75) Ignored: 8 statements, 1 function, 7 branches

All files » lib/consul/ » index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    2031
    1

    1

    1

    1

    1
    1

    1
    150
    150
    150
    1

    149

    1

    237

    237
    79

    237
    237

    1

    77

    77

    77

    77
    77
    77

    1

    3

    3

    3

    3
    3

    1

    74
    74

    74
    80
    1

    80

    80
    74

    74
    80

    80

    77
    77
    77

    3
    3
    3

    1

    1
    84
    84

    84
    84

    1
    73

    1

    84

    84

    84

    74
    74
    74
    1

    73
    73

    84

    84

    84

    46

    38

    10

    28

     var fs = require('fs');
    var path = require('path');

    var logger = require('../logging.js');

    var consul = require('consul')();

    var token = undefined;

    /* istanbul ignore next */
    exports.setToken = function(tok) {
      token = tok;
    }

    var write_content_to_consul = function(key_name, content, cb) {
      logger.trace('Adding key %s, value:\n%s', key_name, content);
      consul.kv.set({'key': key_name, value: content, token: token}, function(err) {
        if (err) {
          return cb('Failed to write key ' + key_name + ' due to ' + err);
        }
        cb();
      });
    };

    /**
     * Given a branch and a file, determine the resource's name in the consul KV store
     */
    var create_key_name = function(branch, file, ref) {
      // Start with repo name so that the subtree is properly namespaced in Consul
      var key_parts = [branch.repo_name];
      // Optionally add the branch name
      if (branch.include_branch_name && !ref) {
        key_parts.push(branch.name);
      }
      // Finish with the file path
      key_parts.push(file);
      return key_parts.join('/');
    };

    /**
     * If a file was modified, read its new value and update consul's KV store.
     */
    var file_modified = function(branch, file, cb) {

      var fqf = branch.branch_directory + path.sep + file;

      logger.trace('Attempting to read "%s"', fqf);

      fs.readFile(fqf, {encoding:'utf8'}, function(err, body) {
        /* istanbul ignore if */
        Iif (err) return cb('Failed to read key ' + fqf + ' due to ' + err);
        var body = body ? body.trim() : '';
        write_content_to_consul(create_key_name(branch, file), body, cb);
      });
    };

    /**
     * If a file was deleted, remove it from consul's KV store.
     */
    var file_deleted = function(branch, file, cb) {
      // Prepend branch name to the KV so that the subtree is properly namespaced in Consuls
      var key_name = create_key_name(branch, file);

      logger.trace('Deleting key %s', key_name);

      consul.kv.del({'key': key_name, token: token}, function(err) {
        /* istanbul ignore if */
        Iif (err) return cb('Failed to delete key ' + key_name + ' due to ' + err);
        cb();
      });
    };

    /**
     * This function expects an array of objects of the following form:
     *
     *   {
     *       'type': 'Any of [AMTDTC]',
     *       'path': 'Path of the file within the repo'
     *   }
     *
     * This function will loop over those objects, handle them, and fire its callback once all records
     * have been processed.  If any errors were noted in the underlying operations, the callback will
     * include the array of errors as the first parameter.
     */
    var process_records = function(branch, records, cb) {

      var pending_records = 0;
      var errors_seen = [];

      var check_pending = function(err) {
        if (err) {
          errors_seen.push(err);
        }

        --pending_records;

        // If there are no pending records, callback with all errors seen, if any.
        if (pending_records === 0) {
          cb((errors_seen.length > 0) ? errors_seen : null);
        }

        // TODO: Add a watchdog timer?  It's a bit scary that this method may never fire its callback
        // if one of the underlying consul operations hangs, especially since the branch is locked
        // waiting for this update to complete.
      };

      records.forEach(function(record) {
        logger.trace('Handling record %s of type %s', record.path, record.type);

        switch (record.type) {
          // Update files that were Added (A), Modified (M), or had their type (i.e. regular file, symlink, submodule, ...) changed (T)
          case 'M':
          case 'A':
          case 'T':
            // Store added/modified file
            ++pending_records;
            file_modified(branch, record.path, check_pending);
            break;
          case 'D':
            // Delete file
            ++pending_records;
            file_deleted(branch, record.path, check_pending);
            break;
          /* istanbul ignore next */
          default:
            logger.error('Unknown git status %s', record.type);
        }
      });
    };

    /**
     * Get the current ref that has been synced with Consul.
     */
    exports.getLastProcessedRef = function(branch, cb) {
      var key_name = create_key_name(branch, branch.name + '.ref', true);
      consul.kv.get({'key': key_name, token: token}, function(err, item) {
        /* istanbul ignore if */
        Iif (err) return cb(err);
        cb(null, item === undefined ? item : item.Value);
      });
    };

    /**
     * Store the current ref that has been synced with Consul.
     */
    exports.setLastProcessedRef = function(branch, ref, cb) {
      write_content_to_consul(create_key_name(branch, branch.name + '.ref', true), ref, cb);
    };

    /**
     * Update consul to match the current state of the branch represented by branch_manager.  In any other case
     * than a fresh clone, we know the prior state of the branch and can determine what files changed since the
     * last time we synced with consul.  In the case of a fresh clone, we just assume that the clone is an accurate
     * representation of what's in consul and add all present files.
     */
    exports.handleRefChange = function(branch, cb) {

      // First, check to see what the current ref is for this branch.
      branch.getCurrentRef(function(err, ref) {
        /* istanbul ignore if */
        Iif (err) return cb("Failed to get current ref for branch " + branch.name + " due to " + err);

        /**
         * Given a set of records, process them and update the branch manager to the current ref if successful.
         */
        var handle_records = function(err, records) {
          /* istanbul ignore if */
          Iif (err) return cb(err);
          process_records(branch, records, function(errs) {
            if (errs) {
              return cb("Some consul updates failed:\n" + errs.join('\n'));
            }
            // Note: This is a bit dangerous.  We only update most recent ref is all consul writes are successful.
            // If there's a bug that causes a certain consul write to always fail, we will always create our diffs
            // from the ref before that file was added.
            exports.setLastProcessedRef(branch, ref, function(err) {
              return cb(err);
            });
          });
        };

        // If there's a ref present on the branch_manager, diff consul between that branch and the current state
        exports.getLastProcessedRef(branch, function(err, last_processed_ref) {
          /* istanbul ignore if */
          Iif (err) return cb(err);

          if (!last_processed_ref) {
            // If there's no ref from which to do a delta update, we just assume all files in the tree are valid values
            // and pass them to consul.
            branch.listAllFiles(handle_records);
          } else {
            if (last_processed_ref === ref) {
              // We have already processed this update.
              return cb(null, "Ref " + ref + " already processed for branch " + branch.name);
            } else {
              // Find diffs between most_recent_ref and current_ref
              branch.listChangedFiles(last_processed_ref, ref, handle_records);
            }
          }
        });
      });
    };