cloudfoundry/cf

View on GitHub
lib/manifests/manifests.rb

Summary

Maintainability
D
2 days
Test Coverage
require "yaml"
require "set"
require "cf/cli/service/create"

require "manifests/loader"

module CFManifests
  MANIFEST_FILE = "manifest.yml"

  @@showed_manifest_usage = false
  @@manifest = nil

  def manifest
    return @@manifest if @@manifest

    if manifest_file && File.exists?(manifest_file)
      @@manifest = load_manifest(manifest_file)
    end
  end

  def save_manifest(save_to = manifest_file)
    fail "No manifest to save!" unless @@manifest

    File.open(save_to, "w") do |io|
      YAML.dump(@@manifest, io)
    end
  end

  # find the manifest file to work with
  def manifest_file
    return @manifest_file if @manifest_file

    unless path = input[:manifest]
      where = Dir.pwd
      while true
        if File.exists?(File.join(where, MANIFEST_FILE))
          path = File.join(where, MANIFEST_FILE)
          break
        elsif File.basename(where) == "/"
          path = nil
          break
        else
          where = File.expand_path("../", where)
        end
      end
    end

    return unless path

    @manifest_file = File.expand_path(path)
  end

  # load and resolve a given manifest file
  def load_manifest(file)
    check_manifest! Loader.new(file, self).manifest
  end

  def check_manifest!(manifest_hash, output = $stdout)
    manifest_hash[:applications].each{ |app| check_attributes!(app, output) }
    manifest_hash
  end

  def check_attributes!(app, output = $stdout)
    app.each do |k, v|
      output.puts error_message_for_attribute(k) unless known_manifest_attributes.include? k
    end
  end

  def error_message_for_attribute(attribute)
    "\e[31mWarning: #{attribute} is not a valid manifest attribute. Please " +
    "remove this attribute from your manifest to get rid of this warning\e[0m"
  end

  def known_manifest_attributes
    [:applications, :buildpack, :command, :disk, :domain, :env,
     :host, :inherit, :instances, :mem, :memory, :name,
     :path, :properties, :runtime, :services, :stack]
  end

  # dynamic symbol resolution
  def resolve_symbol(sym)
    case sym
    when "target-url"
      client_target

    when "target-base"
      target_base

    when "random-word"
      sprintf("%04x", rand(0x0100000))

    when /^ask (.+)/
      ask($1)
    end
  end

  # find apps by an identifier, which may be either a tag, a name, or a path
  def find_apps(identifier)
    return [] unless manifest

    apps = apps_by(:name, identifier)

    if apps.empty?
      apps = apps_by(:path, from_manifest(identifier))
    end

    apps
  end

  # return all the apps described by the manifest
  def all_apps
    manifest[:applications]
  end

  def current_apps
    manifest[:applications].select do |app|
      next unless app[:path]
      from_manifest(app[:path]) == Dir.pwd
    end
  end

  # splits the user's input, resolving paths with the manifest,
  # into internal/external apps
  #
  # internal apps are returned as their data in the manifest
  #
  # external apps are the strings that the user gave, to be
  # passed along wholesale to the wrapped command
  def apps_in_manifest(input = nil, use_name = true, &blk)
    names_or_paths =
      if input.has?(:apps)
        # names may be given but be [], which will still cause
        # interaction, so use #direct instead of #[] here
        input.direct(:apps)
      elsif input.has?(:app)
        [input.direct(:app)]
      elsif input.has?(:name)
        [input.direct(:name)]
      else
        []
      end

    internal = []
    external = []

    names_or_paths.each do |x|
      if x.is_a?(String)
        if x =~ %r([/\\])
          apps = find_apps(File.expand_path(x))

          if apps.empty?
            fail("Path #{b(x)} is not present in manifest #{b(relative_manifest_file)}.")
          end
        else
          apps = find_apps(x)
        end

        if !apps.empty?
          internal += apps
        else
          external << x
        end
      else
        external << x
      end
    end

    [internal, external]
  end

  def create_manifest_for(app, path)
    meta = {
      "name" => app.name,
      "memory" => human_size(app.memory * 1024 * 1024, 0),
      "instances" => app.total_instances,
      "host" => app.host || "none",
      "domain" => app.domain ? app.domain : "none",
      "path" => path
    }

    services = app.services

    unless services.empty?
      meta["services"] = {}

      services.each do |service_instance|
        if service_instance.is_a?(CFoundry::V2::UserProvidedServiceInstance)
          meta["services"][service_instance.name] = {
            "label" => "user-provided",
            "credentials" => service_instance.credentials.stringify_keys,
          }
        else
          service_plan = service_instance.service_plan
          service = service_plan.service

          meta["services"][service_instance.name] = {
            "label" => service.label,
            "provider" => service.provider,
            "version" => service.version,
            "plan" => service_plan.name
          }
        end
      end
    end

    if cmd = app.command
      meta["command"] = cmd
    end

    if buildpack = app.buildpack
      meta["buildpack"] = buildpack
    end

    meta
  end

  private

  def relative_manifest_file
    Pathname.new(manifest_file).relative_path_from(Pathname.pwd)
  end

  def show_manifest_usage
    return if @@showed_manifest_usage

    path = relative_manifest_file
    line "Using manifest file #{c(path, :name)}"
    line

    @@showed_manifest_usage = true
  end

  def no_apps
    fail "No applications or manifest to operate on."
  end

  def warn_reset_changes
    line c("Not applying manifest changes without --reset", :warning)
    line
  end

  def apps_by(attr, val)
    manifest[:applications].select do |info|
      info[attr] == val
    end
  end

  # expand a path relative to the manifest file's directory
  def from_manifest(path)
    File.expand_path(path, File.dirname(manifest_file))
  end


  def ask_to_save(input, app)
    return if manifest_file
    return unless ask("Save configuration?", :default => false)

    manifest = create_manifest_for(app, input[:path])

    with_progress("Saving to #{c("manifest.yml", :name)}") do
      File.open("manifest.yml", "w") do |io|
        YAML.dump(
          { "applications" => [manifest] },
          io)
      end
    end
  end

  def env_hash(val)
    if val.is_a?(Hash)
      val
    else
      hash = {}

      val.each do |pair|
        name, val = pair.split("=", 2)
        hash[name] = val
      end

      hash
    end
  end

  def setup_env(app, info)
    return unless info[:env]
    app.env = env_hash(info[:env])
  end

  def setup_services(app, info)
    return if !info[:services] || info[:services].empty?

    offerings = client.services

    to_bind = []

    info[:services].each do |name, svc|
      name = name.to_s

      if instance = client.service_instance_by_name(name)
        to_bind << instance
      else
        if svc[:label] == "user-provided"
          invoke :create_service,
            name: name,
            offering: CF::Service::UPDummy.new,
            app: app,
            credentials: svc[:credentials]
        else
          offering = offerings.find { |o|
            o.label == (svc[:label] || svc[:type] || svc[:vendor]) &&
              (!svc[:version] || o.version == svc[:version]) &&
              (o.provider == svc[:provider] || (svc[:provider].nil? && o.provider == "core"))
          }

          fail "Unknown service offering: #{svc.inspect}." unless offering

          plan = offering.service_plans.find { |p|
            p.name == (svc[:plan] || "D100")
          }

          fail "Unknown service plan: #{svc[:plan]}." unless plan

          invoke :create_service,
            :name => name,
            :offering => offering,
            :plan => plan,
            :app => app
        end
      end
    end

    to_bind.each do |s|
      next if app.binds?(s)

      # TODO: splat
      invoke :bind_service, :app => app, :service => s
    end
  end

  def target_base
    client_target.sub(/^[^\.]+\./, "")
  end
end