summaryrefslogtreecommitdiff
path: root/lib/chef/node_map.rb
blob: 5befdf25ec8d4ad51c5d658dcea7cf27d31ea11b (plain)
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
#
# Author:: Lamont Granquist (<lamont@chef.io>)
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
# 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.
#

class Chef
  class NodeMap

    VALID_OPTS = [
      :on_platform,
      :on_platforms,
      :platform,
      :os,
      :platform_family,
    ]

    DEPRECATED_OPTS = [
      :on_platform,
      :on_platforms,
    ]

    # Create a new NodeMap
    #
    def initialize
      @map = {}
    end

    # Set a key/value pair on the map with a filter.  The filter must be true
    # when applied to the node in order to retrieve the value.
    #
    # @param key [Object] Key to store
    # @param value [Object] Value associated with the key
    # @param filters [Hash] Node filter options to apply to key retrieval
    # @yield [node] Arbitrary node filter as a block which takes a node argument
    # @return [NodeMap] Returns self for possible chaining
    #
    def set(key, value, filters = {}, &block)
      validate_filter!(filters)
      deprecate_filter!(filters)
      @map[key] ||= []
      # we match on the first value we find, so we want to unshift so that the
      # last setter wins
      # FIXME: need a test for this behavior
      @map[key].unshift({ filters: filters, block: block, value: value })
      self
    end

    # Get a value from the NodeMap via applying the node to the filters that
    # were set on the key.
    #
    # @param node [Chef::Node] The Chef::Node object for the run
    # @param key [Object] Key to look up
    # @return [Object] Value
    #
    def get(node, key)
      # FIXME: real exception
      raise "first argument must be a Chef::Node" unless node.is_a?(Chef::Node)
      return nil unless @map.has_key?(key)
      @map[key].each do |matcher|
        if filters_match?(node, matcher[:filters]) &&
          block_matches?(node, matcher[:block])
          return matcher[:value]
        end
      end
      nil
    end

    def delete(key)
      @map.delete(key)
    end

    private

    # only allow valid filter options
    def validate_filter!(filters)
      filters.each_key do |key|
        # FIXME: real exception
        raise "Bad key #{key} in Chef::NodeMap filter expression" unless VALID_OPTS.include?(key)
      end
    end

    # warn on deprecated filter options
    def deprecate_filter!(filters)
      filters.each_key do |key|
        Chef::Log.warn "The #{key} option to node_map has been deprecated" if DEPRECATED_OPTS.include?(key)
      end
    end

    # @todo: this works fine, but is probably hard to understand
    def negative_match(filter, param)
      # We support strings prefaced by '!' to mean 'not'.  In particular, this is most useful
      # for os matching on '!windows'.
      negative_matches = filter.select { |f| f[0] == '!' }
      return true if !negative_matches.empty? && negative_matches.include?('!' + param)

      # We support the symbol :all to match everything, for backcompat, but this can and should
      # simply be ommitted.
      positive_matches = filter.reject { |f| f[0] == '!' || f == :all }
      return true if !positive_matches.empty? && !positive_matches.include?(param)

      # sorry double-negative: this means we pass this filter.
      false
    end

    def filters_match?(node, filters)
      return true if filters.empty?

      # each filter is applied in turn.  if any fail, then it shortcuts and returns false.
      # if it passes or does not exist it succeeds and continues on.  so multiple filters are
      # effectively joined by 'and'.  all filters can be single strings, or arrays which are
      # effectively joined by 'or'.

      os_filter = [ filters[:os] ].flatten.compact
      unless os_filter.empty?
        return false if negative_match(os_filter, node[:os])
      end

      platform_family_filter = [ filters[:platform_family] ].flatten.compact
      unless platform_family_filter.empty?
        return false if negative_match(platform_family_filter, node[:platform_family])
      end

      # :on_platform and :on_platforms here are synonyms which are deprecated
      platform_filter = [ filters[:platform] || filters[:on_platform] || filters[:on_platforms] ].flatten.compact
      unless platform_filter.empty?
        return false if negative_match(platform_filter, node[:platform])
      end

      return true
    end

    def block_matches?(node, block)
      return true if block.nil?
      block.call node
    end
  end
end