summaryrefslogtreecommitdiff
path: root/lib/chef/audit/runner.rb
blob: 96de022a23c6883b9f9412d07bc0be6061265ccf (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
#
# Author:: Claire McQuin (<claire@chef.io>)
# Copyright:: Copyright 2014-2018, 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 "chef/audit/logger"

class Chef
  class Audit
    class Runner

      attr_reader :run_context
      private :run_context

      def initialize(run_context)
        @run_context = run_context
      end

      def run
        setup
        register_control_groups
        do_run
      end

      def failed?
        RSpec.world.reporter.failed_examples.size > 0
      end

      def num_failed
        RSpec.world.reporter.failed_examples.size
      end

      def num_total
        RSpec.world.reporter.examples.size
      end

      def exclusion_pattern
        Regexp.new(".+[\\\/]lib[\\\/]chef[\\\/]")
      end

      private

      # Prepare to run audits:
      #  - Require files
      #  - Configure RSpec
      #  - Configure Specinfra/Serverspec
      def setup
        require_deps
        configure_rspec
        configure_specinfra
      end

      # RSpec uses a global configuration object, RSpec.configuration. We found
      # there was interference between the configuration for audit-mode and
      # the configuration for our own spec tests in these cases:
      #   1. Specinfra and Serverspec modify RSpec.configuration when loading.
      #   2. Setting output/error streams.
      #   3. Adding formatters.
      #   4. Defining example group aliases.
      #
      # Moreover, Serverspec loads its DSL methods into the global namespace,
      # which causes conflicts with the Chef namespace for resources and packages.
      #
      # We wait until we're in the audit-phase of the chef-client run to load
      # these files. This helps with the namespacing problems we saw, and
      # prevents Specinfra and Serverspec from modifying the RSpec configuration
      # used by our spec tests.
      def require_deps
        require "rspec"
        require "rspec/its"
        require "specinfra"
        require "specinfra/helper"
        require "specinfra/helper/set"
        require "serverspec/helper"
        require "serverspec/matcher"
        require "serverspec/subject"
        require "chef/audit/audit_event_proxy"
        require "chef/audit/rspec_formatter"

        Specinfra::Backend::Cmd.send(:include, Specinfra::Helper::Set)
      end

      # Configure RSpec just the way we like it:
      #   - Set location of error and output streams
      #   - Add custom audit-mode formatters
      #   - Explicitly disable :should syntax
      #   - Set :color option according to chef config
      #   - Disable exposure of global DSL
      def configure_rspec
        set_streams
        add_formatters
        disable_should_syntax

        RSpec.configure do |c|
          c.color = Chef::Config[:color]
          c.expose_dsl_globally = false
          c.project_source_dirs = Array(Chef::Config[:cookbook_path])
          c.backtrace_exclusion_patterns << exclusion_pattern
        end
      end

      # Set the error and output streams which audit-mode will use to report
      # human-readable audit information.
      #
      # This should always be called before #add_formatters. RSpec won't allow
      # the output stream to be changed for a formatter once the formatter has
      # been added.
      def set_streams
        RSpec.configuration.output_stream = Chef::Audit::Logger
        RSpec.configuration.error_stream = Chef::Audit::Logger
      end

      # Add formatters which we use to
      #   1. Output human-readable data to the output stream,
      #   2. Collect JSON data to send back to the analytics server.
      def add_formatters
        RSpec.configuration.add_formatter(Chef::Audit::AuditEventProxy)
        RSpec.configuration.add_formatter(Chef::Audit::RspecFormatter)
        Chef::Audit::AuditEventProxy.events = run_context.events
      end

      # Audit-mode uses RSpec 3. :should syntax is deprecated by default in
      # RSpec 3, so we explicitly disable it here.
      #
      # This can be removed once :should is removed from RSpec.
      def disable_should_syntax
        RSpec.configure do |config|
          config.expect_with :rspec do |c|
            c.syntax = :expect
          end
        end
      end

      # Set up the backend for Specinfra/Serverspec.  :exec is the local system; on Windows, it is :cmd
      def configure_specinfra
        if ChefHelpers.windows?
          Specinfra.configuration.backend = :cmd
          Specinfra.configuration.os = { family: "windows" }
        else
          Specinfra.configuration.backend = :exec
        end
      end

      # Iterates through the control groups registered to this run_context, builds an
      # example group (RSpec::Core::ExampleGroup) object per control group, and
      # registers the group with the RSpec.world.
      #
      # We could just store an array of example groups and not use RSpec.world,
      # but it may be useful later if we decide to apply our own ordering scheme
      # or use example group filters.
      def register_control_groups
        add_example_group_methods
        run_context.audits.each do |name, group| # rubocop:disable Performance/HashEachMethods
          ctl_grp = RSpec::Core::ExampleGroup.__control_group__(*group.args, &group.block)
          RSpec.world.record(ctl_grp)
        end
      end

      # Add example group method aliases to RSpec.
      #
      # __control_group__: Used internally to create example groups from the control
      #               groups saved in the run_context.
      #      control: Used within the context of a control group block, like RSpec's
      #               describe or context.
      def add_example_group_methods
        RSpec::Core::ExampleGroup.define_example_group_method :__control_group__
        RSpec::Core::ExampleGroup.define_example_group_method :control
      end

      # Run the audits!
      def do_run
        # RSpec::Core::Runner wants to be initialized with an
        # RSpec::Core::ConfigurationOptions object, which is used to process
        # command line configuration arguments. We directly fiddle with the
        # internal RSpec configuration object, so we give nil here and let
        # RSpec pick up its own configuration and world.
        runner = RSpec::Core::Runner.new(nil)
        runner.run_specs(RSpec.world.ordered_example_groups)
      end

    end
  end
end