stefan-kolb/nucleus

View on GitHub
lib/nucleus/adapters/v1/cloud_foundry_v2/services.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Nucleus
  module Adapters
    module V1
      class CloudFoundryV2 < Stub
        # Cloud Foundry, operations for the application's addons
        module Services
          # @see Stub#services
          def services
            get('/v2/services?inline-relations-depth=1').body[:resources].collect do |service|
              # show only services that are both, active and bindable
              next unless service[:entity][:active] && service[:entity][:bindable]
              to_nucleus_service(service)
            end.compact
          end

          # @see Stub#service
          def service(service_id_or_name)
            service_guid = service_guid(service_id_or_name)
            to_nucleus_service(get("/v2/services/#{service_guid}?inline-relations-depth=1").body)
          end

          # @see Stub#service_plans
          def service_plans(service_id_or_name)
            service_guid = service_guid(service_id_or_name)
            load_plans(service_guid).collect { |plan| to_nucleus_plan(plan) }
          end

          # @see Stub#service_plan
          def service_plan(service_id_or_name, plan_id)
            service_guid = service_guid(service_id_or_name)
            plan_guid = plan_guid(service_guid, plan_id, Errors::AdapterResourceNotFoundError)
            to_nucleus_plan(get("/v2/service_plans/#{plan_guid}").body)
          end

          # @see Stub#installed_services
          def installed_services(application_name_or_id)
            app_guid = app_guid(application_name_or_id)
            get("/v2/apps/#{app_guid}/service_bindings?inline-relations-depth=1").body[:resources].collect do |binding|
              to_nucleus_installed_service(binding)
            end
          end

          # @see Stub#installed_service
          def installed_service(application_name_or_id, service_id_or_name)
            app_guid = app_guid(application_name_or_id)
            service_guid = service_guid(service_id_or_name)
            cf_binding = binding(app_guid, service_guid)
            # make sure there is a binding
            unless cf_binding
              raise Errors::AdapterResourceNotFoundError,
                    "No such service '#{service_id_or_name}' for application '#{application_name_or_id}'"
            end
            to_nucleus_installed_service(cf_binding)
          end

          # @see Stub#add_service
          def add_service(application_name_or_id, service_entity, plan_entity)
            app_guid = app_guid(application_name_or_id)
            service_guid = service_guid(service_entity[:id], Errors::SemanticAdapterRequestError)
            cf_service = load_allowed_service(service_entity, service_guid)

            # get the plan, throws 422 if the plan could not be found
            plan_guid = plan_guid(service_guid, plan_entity[:id])

            # create new service instance
            instance_request_body = { space_guid: user_space_guid, service_plan_guid: plan_guid,
                                      name: "#{cf_service[:entity][:label]}-#{application_name_or_id}-nucleus" }
            cf_instance = post('/v2/service_instances', body: instance_request_body).body

            # bind the created service instance to the application
            binding_request_body = { service_instance_guid: cf_instance[:metadata][:guid], app_guid: app_guid }
            cf_binding = post('/v2/service_bindings', body: binding_request_body).body

            # created service presentation
            to_nucleus_installed_service(cf_binding, cf_service, cf_instance)
          end

          # @see Stub#change_service
          def change_service(application_name_or_id, service_id, plan_entity)
            app_guid = app_guid(application_name_or_id)
            service_guid = service_guid(service_id)
            cf_service = get("/v2/services/#{service_guid}").body
            fail_with(:service_not_updateable, [service_id]) unless cf_service[:entity][:plan_updateable]

            cf_binding = binding(app_guid, service_guid)

            # get the plan, throws 422 if the plan could not be found
            plan_guid = plan_guid(service_guid, plan_entity[:id])
            cf_instance = put("/v2/service_instances/#{cf_binding[:entity][:service_instance_guid]}",
                              body: { service_plan_guid: plan_guid }).body
            to_nucleus_installed_service(cf_binding, cf_service, cf_instance)
          end

          # @see Stub#remove_service
          def remove_service(application_name_or_id, service_id)
            app_guid = app_guid(application_name_or_id)
            service_guid = service_guid(service_id)
            # sadly we can't resolve the binding and instance from the service_id with ease
            # we therefore setup a chain to resolve the binding and instance from the active plan
            binding = binding(app_guid, service_guid)

            # now remove the binding from the application
            delete("/v2/apps/#{app_guid}/service_bindings/#{binding[:metadata][:guid]}", expects: [201])
            # and finally delete the service instance
            delete("/v2/service_instances/#{binding[:entity][:service_instance_guid]}")
          end

          private

          def load_allowed_service(service_entity, service_guid)
            begin
              cf_service = get("/v2/services/#{service_guid}").body
            rescue Errors::AdapterResourceNotFoundError
              # convert to semantic error with the service being a body, not a path entity
              raise Errors::SemanticAdapterRequestError,
                    "Invalid service: Could not find service with the ID '#{service_entity[:id]}'"
            end

            # must be active and bindable?
            # currently we focus only on bindable services
            fail_with(:service_not_bindable, [service_entity[:id]]) unless cf_service[:entity][:bindable]
            # service must be active, otherwise we can't create the instance
            fail_with(:service_not_active, [service_entity[:id]]) unless cf_service[:entity][:active]
            # service seems to be valid, return
            cf_service
          end

          def remove_all_services(app_guid)
            get("/v2/apps/#{app_guid}/service_bindings").body[:resources].collect do |binding|
              # remove the binding from the application
              delete("/v2/apps/#{app_guid}/service_bindings/#{binding[:metadata][:guid]}", expects: [201])
              # and delete the service instance to prevent orphans
              delete("/v2/service_instances/#{binding[:entity][:service_instance_guid]}")
            end
          end

          def service_guid(service_id_or_name, error_class = Errors::AdapterResourceNotFoundError)
            return service_id_or_name if guid?(service_id_or_name)
            # list all available services
            services = get('/v2/services').body[:resources]
            # find a match and use the service's guid
            service_match = services.find { |service| service[:entity][:label] == service_id_or_name }
            unless service_match
              raise error_class,
                    "Invalid service: Could not find service with name '#{service_id_or_name}'"
            end
            service_match[:metadata][:guid]
          end

          def binding(app_guid, service_id)
            service_plans = get("/v2/services/#{service_id}/service_plans").body[:resources]
            service_plan_ids = service_plans.collect { |plan| plan[:metadata][:guid] }
            app_bindings = get("/v2/apps/#{app_guid}/service_bindings?inline-relations-depth=1").body[:resources]
            # the plan must be bound to the app via an instance
            app_bindings.find do |binding|
              service_plan_ids.include?(binding[:entity][:service_instance][:entity][:service_plan_guid])
            end
          end

          def plan_guid(service_id, plan_name_or_id, error_class = Errors::SemanticAdapterRequestError)
            return plan_name_or_id if guid?(plan_name_or_id)
            # list all plans for the service
            plans = get("/v2/services/#{service_id}/service_plans").body[:resources]
            # find a match and use the plan's guid
            plan_match = plans.find { |plan| plan[:entity][:name] == plan_name_or_id }
            unless plan_match
              raise error_class,
                    "Invalid plan: No such plan '#{plan_name_or_id}' for service '#{service_id}'"
            end
            plan_match[:metadata][:guid]
          end

          def load_plans(service_id)
            get("/v2/services/#{service_id}/service_plans").body[:resources]
          end

          # Memoize this detection.
          # The information is not critical, but takes some time to evaluate.
          # Values are not expected to change often.
          def free_plan?(service_id, plans = nil)
            @free_plans ||= {}
            return @free_plans[service_id] if @free_plans.key?(service_id)
            plans ||= load_plans(service_id)
            @free_plans[service_id] = plans.any? { |plan| plan[:entity][:free] }
            @free_plans[service_id]
          end

          def to_nucleus_plan(cf_plan)
            plan = cf_plan[:entity]
            plan[:id] = cf_plan[:metadata][:guid]
            plan[:created_at] = cf_plan[:metadata][:created_at]
            plan[:updated_at] = cf_plan[:metadata][:updated_at]
            plan[:costs] = []
            plan

            # TODO: determine prices for CF services
            # we know how IBM handles the costs, but can't determine the country
            # we know how Pivotal IO handles the costs
            # but what do the others???

            # extra = Oj.load(plan[:extra])
            # if plan[:free]
            #   plan[:costs] = { period: '', per_instance: false, price: { amount: 0.00, currency: nil} }
            # elsif endpoint_url.include?('pivotal.io')
            #   # show prices for Pivotal Web Services
            #   # see for an explanation: http://docs.pivotal.io/pivotalcf/services/catalog-metadata.html
            #   plan[:costs] = extra[:costs].collect do |cost|
            #     prices = cost[:amount].collect { |currency, amount| { currency: currency, amount: amount } }
            #     { per_instance: false, period: cost[:unit], price: prices }
            #   end
            # elsif endpoint_url.include?('bluemix.net')
            #   # show prices for IBM Bluemix
            # else
            #   # fallback, unknown CF system
            # end
          end

          def to_nucleus_service(cf_service)
            service = cf_service[:entity]
            service = apply_metadata(service, cf_service)
            service[:name] = service.delete(:label)
            service[:release] = service.delete(:version)
            service[:free_plan] = if cf_service[:entity].key?(:service_plans)
                                    # use preloaded plans if available
                                    free_plan?(service[:id], cf_service[:entity][:service_plans])
                                  else
                                    free_plan?(service[:id])
                                  end
            # CF does not have service dependencies
            service[:required_services] = service.delete(:requires)
            # description and documentation_url should already be set
            service
          end

          # Show the installed service. Therefore we need:<br>
          # <ul>
          # <li>the classic service for the basic information (version, name, ...)</li>
          # <li>the binding for the properties and metadata (id, timestamps, ...)</li>
          # <li>the bound instance for the active plan and web_url</li>
          # </ul>
          def to_nucleus_installed_service(cf_binding, cf_service = nil, cf_instance = nil)
            # load if not provided
            cf_instance ||= load_instance(cf_binding)
            cf_service ||= load_service(cf_instance)
            # load if not provided
            cf_service ||= get("/v2/service_plans/#{cf_instance[:entity][:service_plan_guid]}"\
                '?inline-relations-depth=1').body[:entity][:service]

            # active_plan, web_url, properties
            service = to_nucleus_service(cf_service)
            # use the metadata of the binding, is more future proof than instance metadata
            apply_metadata(service, cf_binding)
            service[:active_plan] = cf_instance[:entity][:service_plan_guid]
            service[:web_url] = cf_instance[:entity][:dashboard_url]
            service[:properties] = binding_properties(cf_binding)
            service
          end

          def load_instance(cf_binding)
            if cf_binding[:entity].key?(:service_instance)
              # use if nested property is available
              cf_binding[:entity][:service_instance]
            else
              get("/v2/service_instances/#{cf_binding[:entity][:service_instance_guid]}").body
            end
          end

          def load_service(cf_instance)
            get("/v2/service_plans/#{cf_instance[:entity][:service_plan_guid]}"\
              '?inline-relations-depth=1').body[:entity][:service]
          end

          def binding_properties(binding)
            # in the credentials there are information such as: hostname, username, password, license keys, ...
            binding[:entity][:credentials].collect do |key, value|
              { key: key, value: value, description: nil }
            end
          end

          def apply_metadata(apply_to, cf_object)
            apply_to[:id] = cf_object[:metadata][:guid]
            apply_to[:created_at] = cf_object[:metadata][:created_at]
            apply_to[:updated_at] = cf_object[:metadata][:updated_at]
            apply_to
          end
        end
      end
    end
  end
end