class Azuki::Command::Fork

clone an existing app

Public Instance Methods

index() click to toggle source

fork [NEWNAME]

Fork an existing app – copy config vars and Azuki Postgres data, and re-provision add-ons to a new app. New app name should not be an existing app. The new app will be created as part of the forking process.

-s, –stack STACK # specify a stack –region REGION # HIDDEN: specify a region

# File lib/azuki/command/fork.rb, line 18
def index
  
  from = app
  to = shift_argument || "#{from}-#{(rand*1000).to_i}"

  from_info = api.get_app(from).body

  to_info = action("Creating fork #{to}") do
    api.post_app({
      :name   => to,
      :region => options[:region] || from_info["region"],
      :stack  => options[:stack] || from_info["stack"],
      :tier   => from_info["tier"] == "legacy" ? "production" : from_info["tier"]
    }).body
  end

  action("Copying slug") do
    job_location = cisaurus_client.copy_slug(from, to)
    loop do
      break if cisaurus_client.job_done?(job_location)
      print "."
      sleep 1
    end
  end

  from_config = api.get_config_vars(from).body
  from_addons = api.get_addons(from).body

  from_addons.each do |addon|
    print "Adding #{addon["name"]}... "
    begin
      to_addon = api.post_addon(to, addon["name"]).body
      puts "done"
    rescue Azuki::API::Errors::RequestFailed => ex
      puts "skipped (%s)" % json_decode(ex.response.body)["error"]
    rescue Azuki::API::Errors::NotFound
      puts "skipped (not found)"
    end
    if addon["name"] =~ /^azuki-postgresql:/
      from_var_name = "#{addon["attachment_name"]}_URL"
      from_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1]
      if from_config[from_var_name] == from_config["DATABASE_URL"]
        from_config["DATABASE_URL"] = api.get_config_vars(to).body["#{from_attachment}_URL"]
      end
      from_config.delete(from_var_name)

      plan = addon["name"].split(":").last
      unless %w(dev basic).include? plan
        wait_for_db to, to_addon
      end

      check_for_pgbackups! from
      check_for_pgbackups! to
      migrate_db addon, from, to_addon, to
    end
  end

  to_config = api.get_config_vars(to).body

  action("Copying config vars") do
    diff = from_config.inject({}) do |ax, (key, val)|
      ax[key] = val unless to_config[key]
      ax
    end
    api.put_config_vars to, diff
  end

  puts "Fork complete, view it at #{to_info['web_url']}"
end

Private Instance Methods

check_for_pgbackups!(app) click to toggle source
# File lib/azuki/command/fork.rb, line 95
def check_for_pgbackups!(app)
  unless api.get_addons(app).body.detect { |addon| addon["name"] =~ /^pgbackups:/ }
    action("Adding pgbackups:plus to #{app}") do
      api.post_addon app, "pgbackups:plus"
    end
  end
end
cisaurus_client() click to toggle source
# File lib/azuki/command/fork.rb, line 90
def cisaurus_client
  cisaurus_url = ENV["CISAURUS_HOST"] || "https://cisaurus.azukiapp.com"
  @cisaurus_client ||= Azuki::Client::Cisaurus.new(cisaurus_url)
end
migrate_db(from_addon, from, to_addon, to) click to toggle source
# File lib/azuki/command/fork.rb, line 103
def migrate_db(from_addon, from, to_addon, to)
  transfer = nil

  action("Creating database backup from #{from}") do
    from_config = api.get_config_vars(from).body
    from_attachment = from_addon["attachment_name"]
    pgb = Azuki::Client::Pgbackups.new(from_config["PGBACKUPS_URL"])
    transfer = pgb.create_transfer(from_config["#{from_attachment}_URL"], from_attachment, nil, "BACKUP", :expire => "true")
    error transfer["errors"].values.flatten.join("\n") if transfer["errors"]
    loop do
      transfer = pgb.get_transfer(transfer["id"])
      error transfer["errors"].values.flatten.join("\n") if transfer["errors"]
      break if transfer["finished_at"]
      sleep 1
      print "."
    end
    print " "
  end

  action("Restoring database backup to #{to}") do
    to_config = api.get_config_vars(to).body
    to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1]
    pgb = Azuki::Client::Pgbackups.new(to_config["PGBACKUPS_URL"])
    transfer = pgb.create_transfer(transfer["public_url"], "EXTERNAL_BACKUP", to_config["#{to_attachment}_URL"], to_attachment)
    error transfer["errors"].values.flatten.join("\n") if transfer["errors"]
    loop do
      transfer = pgb.get_transfer(transfer["id"])
      error transfer["errors"].values.flatten.join("\n") if transfer["errors"]
      break if transfer["finished_at"]
      sleep 1
      print "."
    end
    print " "
  end
end
pg_api(starter=false) click to toggle source
# File lib/azuki/command/fork.rb, line 139
def pg_api(starter=false)
  host = starter ? "postgres-starter-api.azukiapp.com" : "postgres-api.azukiapp.com"
  RestClient::Resource.new "https://#{host}/client/v11/databases", Azuki::Auth.user, Azuki::Auth.password
end
wait_for_db(app, attachment) click to toggle source
# File lib/azuki/command/fork.rb, line 144
def wait_for_db(app, attachment)
  attachments = api.get_attachments(app).body.inject({}) { |ax,att| ax.update(att["name"] => att["resource"]["name"]) }
  attachment_name = attachment["message"].match(/Attached as (\w+)_URL\n/)[1]
  action("Waiting for database to be ready") do
    loop do
      begin
        waiting = json_decode(pg_api["#{attachments[attachment_name]}/wait_status"].get.to_s)["waiting?"]
        break unless waiting
        print "."
        sleep 5
      rescue RestClient::ResourceNotFound
      rescue Interrupt
        exit 0
      end
    end
    print " "
  end
end