summaryrefslogtreecommitdiff
path: root/lib/chef/knife/deps.rb
blob: 4cb77eea46adeb813a1f063086e36bf756741760 (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
#
# 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/chef_fs/knife"

class Chef
  class Knife
    class Deps < Chef::ChefFS::Knife
      banner "knife deps PATTERN1 [PATTERNn]"

      category "path-based"

      deps do
        require "chef/chef_fs/file_system"
        require "chef/run_list"
      end

      option :recurse,
        long: "--[no-]recurse",
        boolean: true,
        description: "List dependencies recursively (default: true). Only works with --tree."
      option :tree,
        long: "--tree",
        boolean: true,
        description: "Show dependencies in a visual tree. May show duplicates."
      option :remote,
        long: "--remote",
        boolean: true,
        description: "List dependencies on the server instead of the local filesystem"

      attr_accessor :exit_code

      def run
        if config[:recurse] == false && !config[:tree]
          ui.error "--no-recurse requires --tree"
          exit(1)
        end
        config[:recurse] = true if config[:recurse].nil?

        @root = config[:remote] ? chef_fs : local_fs
        dependencies = {}
        pattern_args.each do |pattern|
          Chef::ChefFS::FileSystem.list(@root, pattern).each do |entry|
            if config[:tree]
              print_dependencies_tree(entry, dependencies)
            else
              print_flattened_dependencies(entry, dependencies)
            end
          end
        end
        exit exit_code if exit_code
      end

      def print_flattened_dependencies(entry, dependencies)
        if !dependencies[entry.path]
          dependencies[entry.path] = get_dependencies(entry)
          dependencies[entry.path].each do |child|
            child_entry = Chef::ChefFS::FileSystem.resolve_path(@root, child)
            print_flattened_dependencies(child_entry, dependencies)
          end
          output format_path(entry)
        end
      end

      def print_dependencies_tree(entry, dependencies, printed = {}, depth = 0)
        dependencies[entry.path] = get_dependencies(entry) if !dependencies[entry.path]
        output "#{'  ' * depth}#{format_path(entry)}"
        if !printed[entry.path] && (config[:recurse] || depth == 0)
          printed[entry.path] = true
          dependencies[entry.path].each do |child|
            child_entry = Chef::ChefFS::FileSystem.resolve_path(@root, child)
            print_dependencies_tree(child_entry, dependencies, printed, depth + 1)
          end
        end
      end

      def get_dependencies(entry)
        if entry.parent && entry.parent.path == "/cookbooks"
          entry.chef_object.metadata.dependencies.keys.map { |cookbook| "/cookbooks/#{cookbook}" }

        elsif entry.parent && entry.parent.path == "/nodes"
          node = Chef::JSONCompat.parse(entry.read)
          result = []
          if node["chef_environment"] && node["chef_environment"] != "_default"
            result << "/environments/#{node['chef_environment']}.json"
          end
          if node["run_list"]
            result += dependencies_from_runlist(node["run_list"])
          end
          result

        elsif entry.parent && entry.parent.path == "/roles"
          role = Chef::JSONCompat.parse(entry.read)
          result = []
          if role["run_list"]
            dependencies_from_runlist(role["run_list"]).each do |dependency|
              result << dependency if !result.include?(dependency)
            end
          end
          if role["env_run_lists"]
            role["env_run_lists"].each_pair do |env, run_list|
              dependencies_from_runlist(run_list).each do |dependency|
                result << dependency if !result.include?(dependency)
              end
            end
          end
          result

        elsif !entry.exists?
          raise Chef::ChefFS::FileSystem::NotFoundError.new(entry)

        else
          []
        end
      rescue Chef::ChefFS::FileSystem::NotFoundError => e
        ui.error "#{format_path(e.entry)}: No such file or directory"
        self.exit_code = 2
        []
      end

      def dependencies_from_runlist(run_list)
        chef_run_list = Chef::RunList.new
        chef_run_list.reset!(run_list)
        chef_run_list.map do |run_list_item|
          case run_list_item.type
          when :role
            "/roles/#{run_list_item.name}.json"
          when :recipe
            if run_list_item.name =~ /(.+)::[^:]*/
              "/cookbooks/#{$1}"
            else
              "/cookbooks/#{run_list_item.name}"
            end
          else
            raise "Unknown run list item type #{run_list_item.type}"
          end
        end
      end
    end
  end
end