summaryrefslogtreecommitdiff
path: root/lib/chef/knife/yaml_convert.rb
blob: ff64f5c53a2a1c30c5cc38f8cc929596ac3a897a (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
#
# Author:: Bryan McLellan <btm@loftninjas.org>
# Copyright:: Copyright (c) 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.
#

require "yaml" unless defined?(YAML)
require_relative "../knife"
class Chef::Knife::YamlConvert < Chef::Knife

  banner "knife yaml convert YAML_FILENAME [RUBY_FILENAME]"

  def run
    if name_args.empty?
      ui.fatal!("Please specify the file name of a YAML recipe to convert to Ruby")
    elsif name_args.size >= 3
      ui.fatal!("knife yaml convert YAML_FILENAME [RUBY_FILENAME]")
    end

    yaml_file = @name_args[0]
    unless ::File.exist?(yaml_file) && ::File.readable?(yaml_file)
      ui.fatal("Input YAML file '#{yaml_file}' does not exist or is unreadable")
    end

    ruby_file = if @name_args[1]
                  @name_args[1] # use the specified output filename if provided
                else
                  if ::File.extname(yaml_file) == ".yml" || ::File.extname(yaml_file) == ".yaml"
                    yaml_file.gsub(/\.(yml|yaml)$/, ".rb")
                  else
                    yaml_file + ".rb" # fall back to putting .rb on the end of whatever the yaml file was named
                  end
                end

    if ::File.exist?(ruby_file)
      ui.fatal!("Output Ruby file '#{ruby_file}' already exists")
    end

    yaml_contents = IO.read(yaml_file)

    # YAML can contain multiple documents (--- is the separator), let's not support that.
    if ::YAML.load_stream(yaml_contents).length > 1
      ui.fatal!("YAML recipe '#{yaml_file}' contains multiple documents, only one is supported")
    end

    # Unfortunately, per the YAML spec, comments are stripped when we load, so we lose them on conversion
    yaml_hash = ::YAML.safe_load(yaml_contents, permitted_classes: [Symbol])
    unless yaml_hash.is_a?(Hash) && yaml_hash.key?("resources")
      ui.fatal!("YAML recipe '#{source_file}' must contain a top-level 'resources' hash (YAML sequence), i.e. 'resources:'")
    end

    ui.warn("No resources found in '#{yaml_file}'") if yaml_hash["resources"].size == 0

    ::File.open(ruby_file, "w") do |file|
      file.write(resource_hash_to_string(yaml_hash["resources"], yaml_file))
    end
    ui.info("Converted '#{yaml_file}' to '#{ruby_file}'")
  end

  # Converts a Hash of resources to a Ruby recipe
  # returns a string ready to be written to a file or stdout
  def resource_hash_to_string(resource_hash, filename)
    ruby_contents = []
    ruby_contents << "# Autoconverted recipe from #{filename}\n"

    resource_hash.each do |r|
      type = r.delete("type")
      name = r.delete("name")

      ruby_contents << "#{type} \"#{name}\" do"
      r.each do |k, v|
        ruby_contents << "  #{k} #{v.inspect}"
      end
      ruby_contents << "end\n"
    end

    ruby_contents.join("\n")
  end
end