File: context.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (188 lines) | stat: -rw-r--r-- 5,561 bytes parent folder | download
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
require_relative '../puppet/thread_local'

# Puppet::Context is a system for tracking services and contextual information
# that puppet needs to be able to run. Values are "bound" in a context when it is created
# and cannot be changed; however a child context can be created, using
# {#override}, that provides a different value.
#
# When binding a {Proc}, the proc is called when the value is looked up, and the result
# is memoized for subsequent lookups. This provides a lazy mechanism that can be used to
# delay expensive production of values until they are needed.
#
# @api private
class Puppet::Context
  require_relative 'context/trusted_information'

  class UndefinedBindingError < Puppet::Error; end
  class StackUnderflow < Puppet::Error; end

  class UnknownRollbackMarkError < Puppet::Error; end
  class DuplicateRollbackMarkError < Puppet::Error; end

  # @api private
  def initialize(initial_bindings)
    @stack = Puppet::ThreadLocal.new(EmptyStack.new.push(initial_bindings))

    # By initializing @rollbacks to nil and creating a hash lazily when #mark or
    # #rollback are called we ensure that the hashes are never shared between
    # threads and it's safe to mutate them
    @rollbacks = Puppet::ThreadLocal.new(nil)
  end

  # @api private
  def push(overrides, description = '')
    @stack.value = @stack.value.push(overrides, description)
  end

  # Push a context and make this global across threads
  # Do not use in a context where multiple threads may already exist
  #
  # @api private
  def unsafe_push_global(overrides, description = '')
    @stack = Puppet::ThreadLocal.new(
      @stack.value.push(overrides, description)
    )
  end

  # @api private
  def pop
    @stack.value = @stack.value.pop
  end

  # @api private
  def lookup(name, &block)
    @stack.value.lookup(name, &block)
  end

  # @api private
  def override(bindings, description = '', &block)
    saved_point = @stack.value
    push(bindings, description)

    yield
  ensure
    @stack.value = saved_point
  end

  # Mark a place on the context stack to later return to with {rollback}.
  #
  # @param name [Object] The identifier for the mark
  #
  # @api private
  def mark(name)
    @rollbacks.value ||= {}
    if @rollbacks.value[name].nil?
      @rollbacks.value[name] = @stack.value
    else
      raise DuplicateRollbackMarkError, _("Mark for '%{name}' already exists") % { name: name }
    end
  end

  # Roll back to a mark set by {mark}.
  #
  # Rollbacks can only reach a mark accessible via {pop}. If the mark is not on
  # the current context stack the behavior of rollback is undefined.
  #
  # @param name [Object] The identifier for the mark
  #
  # @api private
  def rollback(name)
    @rollbacks.value ||= {}
    if @rollbacks.value[name].nil?
      raise UnknownRollbackMarkError, _("Unknown mark '%{name}'") % { name: name }
    end

    @stack.value = @rollbacks.value.delete(name)
  end

  # Base case for Puppet::Context::Stack.
  #
  # @api private
  class EmptyStack
    # Lookup a binding. Since there are none in EmptyStack, this always raises
    # an exception unless a block is passed, in which case the block is called
    # and its return value is used.
    #
    # @api private
    def lookup(name, &block)
      if block
        block.call
      else
        raise UndefinedBindingError, _("Unable to lookup '%{name}'") % { name: name }
      end
    end

    # Base case of #pop always raises an error since this is the bottom
    #
    # @api private
    def pop
      raise(StackUnderflow,
            _('Attempted to pop, but already at root of the context stack.'))
    end

    # Push bindings onto the stack by creating a new Stack object with `self` as
    # the parent
    #
    # @api private
    def push(overrides, description = '')
      Puppet::Context::Stack.new(self, overrides, description)
    end

    # Return the bindings table, which is always empty here
    #
    # @api private
    def bindings
      {}
    end
  end

  # Internal implementation of the bindings stack used by Puppet::Context. An
  # instance of Puppet::Context::Stack represents one level of bindings. It
  # caches a merged copy of all the bindings in the stack up to this point.
  # Each element of the stack is immutable, allowing the base to be shared
  # between threads.
  #
  # @api private
  class Stack
    attr_reader :bindings

    def initialize(parent, bindings, description = '')
      @parent = parent
      @bindings = parent.bindings.merge(bindings || {})
      @description = description
    end

    # Lookup a binding in the current stack. Return the value if it is present.
    # If the value is a stored Proc, evaluate, cache, and return the result. If
    # no binding is found and a block is passed evaluate it and return the
    # result. Otherwise an exception is raised.
    #
    # @api private
    def lookup(name, &block)
      if @bindings.include?(name)
        value = @bindings[name]
        value.is_a?(Proc) ? (@bindings[name] = value.call) : value
      elsif block
        block.call
      else
        raise UndefinedBindingError,
              _("Unable to lookup '%{name}'") % { name: name }
      end
    end

    # Pop one level off the stack by returning the parent object.
    #
    # @api private
    def pop
      @parent
    end

    # Push bindings onto the stack by creating a new Stack object with `self` as
    # the parent
    #
    # @api private
    def push(overrides, description = '')
      Puppet::Context::Stack.new(self, overrides, description)
    end
  end
end