summaryrefslogtreecommitdiff
path: root/lib/chef_zero/chef_data/cookbook_data.rb
blob: 747877a9348eeadf4007023e2df6e48d775fa0e9 (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
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
require "digest/md5"
require "hashie"

module ChefZero
  class Mash < ::Hashie::Mash
    disable_warnings
  end

  module ChefData
    module CookbookData
      class << self
        def to_hash(cookbook, name, version = nil)
          frozen = false
          if cookbook.key?(:frozen)
            frozen = cookbook[:frozen]
            cookbook = cookbook.dup
            cookbook.delete(:frozen)
          end

          result = files_from(cookbook)
          recipe_names = result[:all_files].select do |file|
            file[:name].start_with?("recipes/")
          end.map do |recipe|
            recipe_name = recipe[:name][0..-2]
            recipe_name == "default" ? name : "#{name}::#{recipe_name}"
          end
          result[:metadata] = metadata_from(cookbook, name, version, recipe_names)
          result[:name] = "#{name}-#{result[:metadata][:version]}"
          result[:json_class] = "Chef::CookbookVersion"
          result[:cookbook_name] = name
          result[:version] = result[:metadata][:version]
          result[:chef_type] = "cookbook_version"
          result[:frozen?] = true if frozen
          result
        end

        def metadata_from(directory, name, version, recipe_names)
          metadata = PretendCookbookMetadata.new(PretendCookbook.new(name, recipe_names))
          # If both .rb and .json exist, read .json
          if has_child(directory, "metadata.json")
            metadata.from_json(read_file(directory, "metadata.json"))
          elsif has_child(directory, "metadata.rb")
            begin
              file = filename(directory, "metadata.rb") || "(#{name}/metadata.rb)"
              metadata.instance_eval(read_file(directory, "metadata.rb"), file)
            rescue
              ChefZero::Log.error("Error loading cookbook #{name}: #{$!}\n  #{$!.backtrace.join("\n  ")}")
            end
          end
          result = {}
          metadata.to_hash.each_pair do |key, value|
            result[key.to_sym] = value
          end
          result[:version] = version if version
          result
        end

        private

        def files_from(directory)
          # TODO some support .rb only
          result = load_files(directory)

          set_specificity(result, :templates)
          set_specificity(result, :files)

          result = {
            all_files: result,
          }
          result
        end

        def has_child(directory, name)
          if directory.is_a?(Hash)
            directory.key?(name)
          else
            directory.child(name).exists?
          end
        end

        def read_file(directory, name)
          if directory.is_a?(Hash)
            directory[name]
          else
            directory.child(name).read
          end
        end

        def filename(directory, name)
          if directory.respond_to?(:file_path)
            File.join(directory.file_path, name)
          else
            nil
          end
        end

        def get_directory(directory, name)
          if directory.is_a?(Hash)
            directory[name].is_a?(Hash) ? directory[name] : nil
          else
            result = directory.child(name)
            result.dir? ? result : nil
          end
        end

        def list(directory)
          if directory.is_a?(Hash)
            directory.keys
          else
            directory.children.map(&:name)
          end
        end

        def load_child_files(parent, key, recursive, part)
          result = load_files(get_directory(parent, key), recursive, part)
          result.each do |file|
            file[:path] = "#{key}/#{file[:path]}"
          end
          result
        end

        def load_files(directory, recursive = true, part = nil)
          result = []
          if directory
            list(directory).each do |child_name|
              dir = get_directory(directory, child_name)
              if dir
                child_part = child_name if part.nil?
                if recursive
                  result += load_child_files(directory, child_name, recursive, child_part)
                end
              else
                result += load_file(read_file(directory, child_name), child_name, part)
              end
            end
          end
          result
        end

        def load_file(value, name, part = nil)
          specific_name = part ? "#{part}/#{name}" : name
          [{
            name: specific_name,
            path: name,
            checksum: Digest::MD5.hexdigest(value),
            specificity: "default",
          }]
        end

        def set_specificity(files, type)
          files.each do |file|
            next unless file[:name].split("/")[0] == type.to_s

            parts = file[:path].split("/")
            file[:specificity] = if parts.size == 2
                                   "default"
                                 else
                                   parts[1]
                                 end
          end
        end
      end

      # Just enough cookbook to make a Metadata object
      class PretendCookbook
        def initialize(name, fully_qualified_recipe_names)
          @name = name
          @fully_qualified_recipe_names = fully_qualified_recipe_names
        end
        attr_reader :name, :fully_qualified_recipe_names
      end

      # Handles loading configuration values from a Chef config file
      #
      # @author Justin Campbell <justin.campbell@riotgames.com>
      class PretendCookbookMetadata < Hash
        # @param [String] path
        def initialize(cookbook)
          name(cookbook.name)
          recipes(cookbook.fully_qualified_recipe_names)
          %w{attributes grouping dependencies supports recommendations suggestions conflicting providing replacing recipes}.each do |hash_arg|
            self[hash_arg.to_sym] = Mash.new
          end
        end

        def from_json(json)
          merge!(FFI_Yajl::Parser.parse(json))
        end

        private

        def depends(cookbook, *version_constraints)
          cookbook_arg(:dependencies, cookbook, version_constraints)
        end

        def supports(cookbook, *version_constraints)
          cookbook_arg(:supports, cookbook, version_constraints)
        end

        def provides(cookbook, *version_constraints)
          cookbook_arg(:providing, cookbook, version_constraints)
        end

        def gem(*opts)
          self[:gems] ||= []
          self[:gems] << opts
        end

        def recipe(recipe, description)
          self[:recipes][recipe] = description
        end

        def attribute(name, options)
          self[:attributes][name] = options
        end

        def cookbook_arg(key, cookbook, version_constraints)
          self[key][cookbook] = version_constraints.first || ">= 0.0.0"
        end

        def method_missing(key, *values)
          if values.nil?
            self[key.to_sym]
          else
            if values.length > 1
              store key.to_sym, values
            else
              store key.to_sym, values.first
            end
          end
        end
      end
    end
  end

  CookbookData = ChefData::CookbookData
end