service.rb 9.1 KB
Newer Older
1
require "fog/core/utils"
2

3
module Fog
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
  def self.services
    @services ||= {}
  end

  class Service
    class Error < Fog::Errors::Error; end
    class NotFound < Fog::Errors::NotFound; end

    module NoLeakInspector
      def inspect
        "#<#{self.class}:#{self.object_id} #{(self.instance_variables - service.secrets).map {|iv| [iv, self.instance_variable_get(iv).inspect].join('=')}.join(' ')}>"
      end
    end

    module Collections
      def collections
        service.collections
      end

      def mocked_requests
        service.mocked_requests
      end

      def requests
        service.requests
      end
    end

    class << self
      def inherited(child)
        child.class_eval <<-EOS, __FILE__, __LINE__
          class Error < Fog::Service::Error; end
          class NotFound < Fog::Service::NotFound; end

          module Collections
            include Fog::Service::Collections

            def service
              #{child}
            end
          end

          def self.service
            #{child}
          end
        EOS
      end

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
      # {Fog::Service} is (unfortunately) both a builder class and the subclass for any fog service.
      #
      # Creating a {new} instance using the builder will return either an instance of
      # +Fog::<Service>::<Provider>::Real+ or +Fog::<Service>::<Provider>::Mock+ based on the value
      # of {Fog.mock?} when the builder is used.
      #
      # Each provider can require or recognize different settings (often prefixed with the providers
      # name). These settings map to keys in the +~/.fog+ file.
      #
      # Settings can be passed as either a Hash or an object that responds to +config_service?+ with
      # +true+. This object will be passed through unchanged to the +Real+ or +Mock+ service that is
      # created. It is up to providers to adapt services to use these config objects.
      #
      # @abstract Subclass and implement real or mock code
      #
      # @param [Hash,#config_service?] config
      #   Settings or an object used to build a service instance
      # @option config [Hash] :headers
      #   Passed to the underlying {Fog::Core::Connection} unchanged
      #
      # @return [Fog::Service::Provider::Real] if created while mocking is disabled
      # @return [Fog::Service::Provider::Mock] if created while mocking is enabled
      # @raise [ArgumentError] if a setting required by the provider was not passed in
      #
      # @example Minimal options (dependent on ~/.fog)
      #   @service = Fog::Compute::Example.new # => <#Fog::Compute::Example::Real>
      #
      # @example Mocked service
      #   Fog.mock!
      #   @service = Fog::Compute::Example.new # => <#Fog::Compute::Example::Mock>
      #
      # @example Configured using many options (options merged into ~/.fog)
      #   @options = {
      #     :example_username => "fog",
      #     :example_password => "fog"
      #   }
      #   @service = Fog::Compute::Example.new(@options)
      #
      # @example Configured using external config object (~/.fog ignored completely)
      #   @config = Fog::Example::Config.new(...)
      #   @service = Fog::Compute::Example.new(@config)
      #
      def new(config = {})
        if config.respond_to?(:config_service?) && config.config_service?
          cleaned_settings = config
        else
          cleaned_settings = handle_settings(config)
        end
100
101
        setup_requirements

102
        svc = service
103
        if Fog.mocking?
104
105
106
107
108
          while svc != Fog::Service
            service::Mock.send(:include, svc::Collections)
            svc = svc.superclass
          end
          service::Mock.new(cleaned_settings)
109
        else
110
111
112
113
          while svc != Fog::Service
            service::Real.send(:include, svc::Collections)
            svc = svc.superclass
          end
114
          service::Real.send(:include, service::NoLeakInspector)
115
          service::Real.new(cleaned_settings)
116
117
118
        end
      end

119
      # @deprecated
120
121
122
      def fetch_credentials(options)
        # attempt to load credentials from config file
        begin
123
          Fog.credentials.reject { |key, value| !(recognized | requirements).include?(key) }
124
125
126
127
128
129
130
131
132
133
134
135
136
        rescue LoadError
          # if there are no configured credentials, do nothing
          {}
        end
      end

      def setup_requirements
        if superclass.respond_to?(:setup_requirements)
          superclass.setup_requirements
        end

        @required ||= false
        unless @required
137
138
139
          require_models
          require_collections_and_define
          require_requests_and_mock
140
141
142
143
          @required = true
        end
      end

144
      # @note This path is used to require model and collection files
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
      def model_path(new_path)
        @model_path = new_path
      end

      def collection(new_collection)
        collections << new_collection
      end

      def collections
        @collections ||= []
      end

      def coerce_options(options)
        options.each do |key, value|
          value_string = value.to_s.downcase
          if value.nil?
            options.delete(key)
          elsif value == value_string.to_i.to_s
            options[key] = value.to_i
          else
            options[key] = case value_string
            when 'false'
              false
            when 'true'
              true
            else
              value
            end
          end
        end
      end

      def mocked_requests
        @mocked_requests ||= []
      end

      def model(new_model)
        models << new_model
      end

      def models
        @models ||= []
      end

      def request_path(new_path)
        @request_path = new_path
      end

      def request(new_request)
        requests << new_request
      end

      def requests
        @requests ||= []
      end

      def secrets(*args)
        if args.empty?
          @secrets ||= []
        else
          args.inject(secrets) do |secrets, secret|
            secrets << "@#{secret}".to_sym
          end
        end
      end

      def requires(*args)
        requirements.concat(args)
      end

      def requirements
        @requirements ||= []
      end

      def recognizes(*args)
        recognized.concat(args)
      end

      def recognized
        @recognized ||= [:connection_options]
      end

      def validate_options(options)
        keys = []
        for key, value in options
          unless value.nil?
            keys << key
          end
        end
        missing = requirements - keys
235

236
237
238
239
240
241
242
243
244
245
246
247
        unless missing.empty?
          raise ArgumentError, "Missing required arguments: #{missing.join(', ')}"
        end

        unless recognizes.empty?
          unrecognized = options.keys - requirements - recognized
          unless unrecognized.empty?
            Fog::Logger.warning("Unrecognized arguments: #{unrecognized.join(', ')}")
          end
        end
      end

248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
      private

      # This is the original way service settings were handled. Settings from +~/.fog+ were merged
      # together with the passed options, keys are turned to symbols and coerced into Boolean or
      # Fixnums.
      #
      # If the class has declared any required settings then {ArgumentError} will be raised.
      #
      # Any setting that is not whitelisted will cause a warning to be output.
      #
      def handle_settings(settings)
        combined_settings = fetch_credentials(settings).merge(settings)
        prepared_settings = Fog::Core::Utils.prepare_service_settings(combined_settings)
        validate_options(prepared_settings)
        coerce_options(prepared_settings)
      end

      # This will attempt to require all model files declared by the service using fog's DSL
      def require_models
        models.each do |model|
          require File.join(@model_path, model.to_s)
        end
      end

      def require_collections_and_define
        collections.each do |collection|
          require File.join(@model_path, collection.to_s)
          constant = camel_case_collection_name(collection)
          service::Collections.module_eval <<-EOS, __FILE__, __LINE__
            def #{collection}(attributes = {})
              #{service}::#{constant}.new({ :service => self }.merge(attributes))
            end
          EOS
        end
      end

      # This converts names of collections from Symbols as defined in the DSL (+:database_server+)
      # into CamelCase version (+DatabaseServer+) for metaprogramming skulduggery.
      #
      # @param [String,Symbol] collection The name of the collection broken with underscores
      # @return [String] in camel case
      def camel_case_collection_name(collection)
        collection.to_s.split('_').map(&:capitalize).join
      end
292

293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
      # This will attempt to require all request files declared in the service using fog's DSL
      def require_requests_and_mock
        requests.each do |request|
          require File.join(@request_path, request.to_s)
          if service::Mock.method_defined?(request)
            mocked_requests << request
          else
            service::Mock.module_eval <<-EOS, __FILE__, __LINE__
              def #{request}(*args)
                Fog::Mock.not_implemented
              end
            EOS
          end
        end
      end
    end
309
310
  end
end