summaryrefslogtreecommitdiff
path: root/lib/chef/resource/template.rb
blob: e3f266740dfec70734b53c6f523a2c9f7684f535 (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
#
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Seth Chisamore (<schisamo@chef.io>)
# Author:: Tyler Cloke (<tyler@chef.io>)
# 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_relative "file"
require_relative "../mixin/securable"
require "chef-utils/dist" unless defined?(ChefUtils::Dist)

class Chef
  class Resource
    # A cookbook template is an Embedded Ruby (ERB) template that is used to dynamically generate static text files.
    # Templates may contain Ruby expressions and statements, and are a great way to manage configuration files. Use the
    # template resource to add cookbook templates to recipes; place the corresponding Embedded Ruby (ERB) template file
    # in a cookbook's /templates directory.
    #
    # Use the template resource to manage the contents of a file using an Embedded Ruby (ERB) template by transferring
    # files from a sub-directory of COOKBOOK_NAME/templates/ to a specified path located on a host that is running the
    # chef-client. This resource includes actions and properties from the file resource. Template files managed by the
    # template resource follow the same file specificity rules as the remote_file and file resources.
    class Template < Chef::Resource::File
      unified_mode true

      provides :template

      include Chef::Mixin::Securable

      attr_reader :inline_helper_blocks
      attr_reader :inline_helper_modules

      def initialize(name, run_context = nil)
        super
        @source = "#{::File.basename(name)}.erb"
        @inline_helper_blocks = {}
        @inline_helper_modules = []
        @helper_modules = []
      end

      def source(file = nil)
        set_or_return(
          :source,
          file,
          kind_of: [ String, Array ]
        )
      end

      property :variables, Hash,
        description: "The variables property of the template resource can be used to reference a partial template file by using a Hash.",
        default: lazy { {} }

      property :cookbook, String,
        description: "The cookbook in which a file is located (if it is not located in the current cookbook). The default value is the current cookbook.",
        desired_state: false

      property :local, [ TrueClass, FalseClass ],
        default: false, desired_state: false,
        description: "Load a template from a local path. By default, the #{ChefUtils::Dist::Infra::CLIENT} loads templates from a cookbook's /templates directory. When this property is set to true, use the source property to specify the path to a template on the local node."

      # Declares a helper method to be defined in the template context when
      # rendering.
      #
      # === Example:
      #
      # ==== Basic usage:
      # Given the following helper:
      #   helper(:static_value) { "hello from helper" }
      # A template with the following code:
      #   <%= static_value %>
      # Will render as;
      #   hello from helper
      #
      # ==== Referencing Instance Variables:
      # Any instance variables available to the template can be referenced in
      # the method body. For example, you can simplify accessing app-specific
      # node attributes like this:
      #   helper(:app) { @node[:my_app_attributes] }
      # And use it in a template like this:
      #   <%= app[:listen_ports] %>
      # This is equivalent to the non-helper template code:
      #   <%= @node[:my_app_attributes][:listen_ports] %>
      #
      # ==== Method Arguments:
      # Helper methods can also take arguments. The syntax available for
      # argument specification supports full syntax available for method
      # definition.
      #
      # Continuing the above example of simplifying attribute access, we can
      # define a helper to look up app-specific attributes like this:
      #   helper(:app) { |setting| @node[:my_app_attributes][setting] }
      # The template can then look up attributes like this:
      #   <%= app(:listen_ports) %>
      def helper(method_name, &block)
        unless block_given?
          raise Exceptions::ValidationFailed,
            "`helper(:method)` requires a block argument (e.g., `helper(:method) { code }`)"
        end

        unless method_name.is_a?(Symbol)
          raise Exceptions::ValidationFailed,
            "method_name argument to `helper(method_name)` must be a symbol (e.g., `helper(:method) { code }`)"
        end

        @inline_helper_blocks[method_name] = block
      end

      # Declares a module to define helper methods in the template's context
      # when rendering. There are two primary forms.
      #
      # === Inline Module Definition
      # When a block is given, the block is used to define a module which is
      # then mixed in to the template context w/ `extend`.
      #
      # ==== Inline Module Example
      # Given the following code in the template resource:
      #   helpers do
      #     # Add "syntax sugar" for referencing app-specific attributes
      #     def app(attribute)
      #       @node[:my_app_attributes][attribute]
      #     end
      #   end
      # You can use it in the template like so:
      #   <%= app(:listen_ports) %>
      # Which is equivalent to:
      #   <%= @node[:my_app_attributes][:listen_ports] %>
      #
      # === External Module Form
      # When a module name is given, the template context will be extended with
      # that module. This is the recommended way to customize template contexts
      # when you need to define more than an handful of helper functions (but
      # also try to keep your template helpers from getting out of hand--if you
      # have very complex logic in your template helpers, you should further
      # extract your code into separate libraries).
      #
      # ==== External Module Example
      # To extract the above inline module code to a library, you'd create a
      # library file like this:
      #   module MyTemplateHelper
      #     # Add "syntax sugar" for referencing app-specific attributes
      #     def app(attribute)
      #       @node[:my_app_attributes][attribute]
      #     end
      #   end
      # And in the template resource:
      #   helpers(MyTemplateHelper)
      # The template code in the above example will work unmodified.
      def helpers(module_name = nil, &block)
        if block_given? && !module_name.nil?
          raise Exceptions::ValidationFailed,
            "Passing both a module and block to #helpers is not supported. Call #helpers multiple times instead"
        elsif block_given?
          @inline_helper_modules << block
        elsif module_name.is_a?(::Module)
          @helper_modules << module_name
        elsif module_name.nil?
          raise Exceptions::ValidationFailed,
            "#helpers requires either a module name or inline module code as a block.\n" +
              "e.g.: helpers do; helper_code; end;\n" +
              "OR: helpers(MyHelpersModule)"
        else
          raise Exceptions::ValidationFailed,
            "Argument to #helpers must be a module. You gave #{module_name.inspect} (#{module_name.class})"
        end
      end

      # Compiles all helpers from inline method definitions, inline module
      # definitions, and external modules into an Array of Modules. The context
      # object for the template is extended with these modules to provide
      # per-resource template logic.
      def helper_modules
        compiled_helper_methods + compiled_helper_modules + @helper_modules
      end

      private

      # compiles helper methods into a module that can be included in template context
      def compiled_helper_methods
        if inline_helper_blocks.empty?
          []
        else
          resource_helper_blocks = inline_helper_blocks
          helper_mod = Module.new do
            resource_helper_blocks.each do |method_name, method_body|
              define_method(method_name, &method_body)
            end
          end
          [ helper_mod ]
        end
      end

      def compiled_helper_modules
        @inline_helper_modules.map do |module_body|
          Module.new(&module_body)
        end
      end

    end
  end
end