zuazo/chef-encrypted-attributes

View on GitHub
lib/chef/encrypted_attribute/remote_node.rb

Summary

Maintainability
A
35 mins
Test Coverage
# encoding: UTF-8
#
# Author:: Xabier de Zuazo (<xabier@zuazo.org>)
# Copyright:: Copyright (c) 2014-2015 Onddo Labs, SL.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'chef/mixin/params_validate'
require 'chef/encrypted_attribute/search_helper'
require 'chef/encrypted_attribute/cache_lru'

class Chef
  class EncryptedAttribute
    # Remote Node object to read and save node attributes remotely.
    class RemoteNode
      include ::Chef::Mixin::ParamsValidate
      include ::Chef::EncryptedAttribute::SearchHelper

      # Remote Node object constructor.
      #
      # @param name [String] node name.
      def initialize(name)
        name(name)
      end

      # Remote node attribute values cache.
      #
      # It is disabled by default. You can enable it changing it's size:
      #
      # ```ruby
      # Chef::EncryptedAttribute::RemoteNode.cache.max_size(1024)
      # ```
      #
      # @return [CacheLru] node attributes LRU cache.
      def self.cache
        # disabled by default
        @@cache ||= Chef::EncryptedAttribute::CacheLru.new(0)
      end

      # Read or set node name.
      #
      # @param arg [String] node name.
      # @return [String] node name.
      def name(arg = nil)
        # TODO: clean the cache when changed?
        set_or_return(
          :name,
          arg,
          kind_of: String
        )
      end

      # Loads a remote node attribute.
      #
      # @param attr_ary [Array<String>] node attribute path as Array.
      # @param rows [Integer] maximum number of rows to return in searches.
      # @param partial_search [Boolean] whether to use partial search.
      # @return [Mixed] node attribute value, `nil` if not found.
      # @raise [ArgumentError] if the attribute path format is wrong.
      # @raise [SearchFailure] if there is a Chef search error.
      # @raise [SearchFatalError] if the Chef search response is wrong.
      # @raise [InvalidSearchKeys] if search keys structure is wrong.
      def load_attribute(attr_ary, rows = 1000, partial_search = true)
        assert_attribute_array(attr_ary)
        cache_key = cache_key(name, attr_ary)
        return self.class.cache[cache_key] if self.class.cache.key?(cache_key)
        keys = { 'value' => attr_ary }
        res = search_by_name(:node, @name, keys, rows, partial_search)
        self.class.cache[cache_key] = parse_search_result(res)
      end

      # Saves a remote node attribute.
      #
      # @param attr_ary [Array<String>] node attribute path as Array.
      # @param value [Mixed] node attribute value to set.
      # @return [Mixed] node attribute value.
      # @raise [ArgumentError] if the attribute path format is wrong.
      def save_attribute(attr_ary, value)
        assert_attribute_array(attr_ary)
        cache_key = cache_key(name, attr_ary)

        node = Chef::Node.load(name)
        last = attr_ary.pop
        node_attr = attr_ary.reduce(node.normal) do |a, k|
          a[k] = Mash.new unless a.key?(k)
          a[k]
        end
        node_attr[last] = value

        node.save
        self.class.cache[cache_key] = value
      end

      # Deletes a remote node attribute.
      #
      # @param attr_ary [Array<String>] node attribute path as Array.
      # @return [Boolean] whether the node attribute has been found and
      #   successfully deleted.
      # @raise [ArgumentError] if the attribute path format is wrong.
      def delete_attribute(attr_ary)
        assert_attribute_array(attr_ary)
        cache_key = cache_key(name, attr_ary)

        node = Chef::Node.load(name)
        last = attr_ary.pop
        node_attr = attr_ary.reduce(node.normal) do |a, k|
          a.respond_to?(:key?) && a.key?(k) ? a[k] : nil
        end
        if node_attr.respond_to?(:key?) && node_attr.key?(last)
          node_attr.delete(last)
          node.save
          self.class.cache.delete(cache_key)
          true
        else
          false
        end
      end

      protected

      # Parses {SearchHelper#search} result.
      #
      # @param res [Array<Hash>] {SearchHelper#search} result.
      # @return [Mixed] final search result value.
      def parse_search_result(res)
        unless res.is_a?(Array) && res[0].is_a?(Hash) && res[0].key?('value')
          return nil
        end
        res[0]['value']
      end

      # Generates the cache key.
      #
      # @param name [String] node name.
      # @param attr_ary [Array<String>] node attribute path as Array.
      # @return [String] cache key.
      def cache_key(name, attr_ary)
        "#{name}:#{attr_ary.inspect}" # TODO: OK, this can be improved
      end

      # Asserts that the attribute path is in the correct format.
      #
      # @param attr_ary [Array<String>] node attribute path as Array.
      # @return void
      # @raise [ArgumentError] if the attribute path format is wrong.
      def assert_attribute_array(attr_ary)
        return if attr_ary.is_a?(Array)
        fail ArgumentError,
             "#{self.class}##{__method__} attr_ary argument must be an array "\
             "of strings. You passed #{attr_ary.inspect}."
      end
    end
  end
end