class Seira::Db::Create

Attributes

action[R]
app[R]
args[R]
context[R]
cpu[R]
make_highly_available[R]
memory[R]
name[R]
prefix[R]
proxyuser_password[R]
replica_for[R]
root_password[R]
storage[R]
version[R]

Public Class Methods

new(app:, action:, args:, context:) click to toggle source
# File lib/seira/db/create.rb, line 12
def initialize(app:, action:, args:, context:)
  @app = app
  @action = action
  @args = args
  @context = context

  # We allow overriding the version, so you could specify a mysql version but much of the
  # below assumes postgres for now
  @version = 'POSTGRES_9_6'
  @cpu = 1 # Number of CPUs
  @memory = 4 # GB
  @storage = 10 # GB
  @replica_for = nil
  @make_highly_available = false

  @root_password = nil
  @proxyuser_password = nil
end

Public Instance Methods

add(existing_instances) click to toggle source
# File lib/seira/db/create.rb, line 39
def add(existing_instances)
  if !args.empty? && (args[0].start_with? '--prefix=')
    @prefix = args[0].split('=')[1]
  end

  if prefix.nil?
    puts "missing --prefix= for add command. Must be the first argument."
    exit(1)
  end

  # remove prefix from the head of the list since we don't want to pass it to gcloud
  args.pop

  @name = "#{app}-#{prefix}-#{Seira::Random.unique_name(existing_instances)}"
  puts "Attempting to create #{name}"
  run_create_command

  update_root_password
  create_proxy_user

  secrets_name = "#{name}-credentials"
  kubectl("create secret generic  #{secrets_name} --from-literal=ROOT_PASSWORD=#{root_password} --from-literal=PROXYUSER_PASSWORD=#{proxyuser_password}", context: context)
  puts "Credentials were saved in #{secrets_name}"
end
configure(instance_name, master_name) click to toggle source
# File lib/seira/db/create.rb, line 64
def configure(instance_name, master_name)
  @name = instance_name
  @replica_for = master_name

  configure_created_db
end
run(existing_instances) click to toggle source
# File lib/seira/db/create.rb, line 31
def run(existing_instances)
  @name = "#{app}-#{Seira::Random.unique_name(existing_instances)}"

  run_create_command

  configure_created_db
end

Private Instance Methods

configure_created_db() click to toggle source
# File lib/seira/db/create.rb, line 73
def configure_created_db
  if replica_for.nil?
    update_root_password
    create_proxy_user
  end

  set_secrets

  puts "To use this database, use write-pgbouncer-yaml command and deploy the pgbouncer config file that was created and use the ENV that was set."
  if replica_for.nil?
    puts "After deploying, make sure to run alter-proxyuser-roles for the created instance."
  end
  puts "To make this database the primary, promote it using the CLI and update the DATABASE_URL."
end
create_pgbouncer_secret(db_user:, db_password:) click to toggle source
# File lib/seira/db/create.rb, line 200
def create_pgbouncer_secret(db_user:, db_password:)
  kubectl("create secret generic #{pgbouncer_secret_name} --from-literal=DB_USER=#{db_user} --from-literal=DB_PASSWORD=#{db_password}", context: context)
end
create_proxy_user() click to toggle source
# File lib/seira/db/create.rb, line 167
def create_proxy_user
  # Create proxyuser with secure password
  @proxyuser_password = SecureRandom.urlsafe_base64(32)

  if gcloud("sql users create proxyuser --instance=#{name} --password=#{proxyuser_password}", context: context, format: :boolean)
    puts "Created proxyuser with password #{proxyuser_password}"
  else
    puts "Failed to create proxyuser"
    exit(1)
  end
end
default_database_name() click to toggle source
# File lib/seira/db/create.rb, line 220
def default_database_name
  "#{app}_#{Helpers.rails_env(context: context)}"
end
ips() click to toggle source
# File lib/seira/db/create.rb, line 224
def ips
  @ips ||= Helpers.sql_ips(name, context: context)
end
pgbouncer_secret_name() click to toggle source
# File lib/seira/db/create.rb, line 208
def pgbouncer_secret_name
  "#{name}-pgbouncer-secrets"
end
pgbouncer_service_name() click to toggle source
# File lib/seira/db/create.rb, line 212
def pgbouncer_service_name
  "#{name}-pgbouncer-service"
end
pgbouncer_tier() click to toggle source
# File lib/seira/db/create.rb, line 216
def pgbouncer_tier
  name.gsub("#{app}-", "")
end
run_create_command() click to toggle source
# File lib/seira/db/create.rb, line 88
def run_create_command
  # The 'beta' is needed for HA and other beta features
  create_command = "beta sql instances create #{name}"

  args.each do |arg|
    if arg.start_with? '--version='
      @version = arg.split('=')[1]
    elsif arg.start_with? '--cpu='
      @cpu = arg.split('=')[1]
    elsif arg.start_with? '--memory='
      @memory = arg.split('=')[1]
    elsif arg.start_with? '--storage='
      @storage = arg.split('=')[1]
    elsif arg.start_with? '--primary='
      @replica_for = arg.split('=')[1] # TODO: Read secret to get it automatically, but allow for fallback
    elsif arg.start_with? '--highly-available'
      @make_highly_available = true
    elsif arg.start_with? '--database-name='
      @database_name = arg.split('=')[1]
    elsif /^--[\w\-]+=.+$/.match? arg
      create_command += " #{arg}"
    else
      puts "Warning: Unrecognized argument '#{arg}'"
    end
  end

  if make_highly_available && !replica_for.nil?
    puts "Cannot create an HA read-replica."
    exit(1)
  end

  # Basic configs
  create_command += " --database-version=#{version}"
  create_command += " --network=default" # allow network to be configurable?
  create_command += " --no-assign-ip" # don't assign public ip

  # A read replica cannot have HA, inherits the cpu, mem and storage of its primary
  if replica_for.nil?
    # Make sure to do automated daily backups by default, unless it's a replica
    create_command += " --backup"
    create_command += " --cpu=#{cpu}"
    create_command += " --memory=#{memory}"
    create_command += " --storage-size=#{storage}"

    # Make HA if asked for
    create_command += " --availability-type=REGIONAL" if make_highly_available
  else
    create_command += " --master-instance-name=#{replica_for}"
    # We don't need to wait for it to finish to move ahead if it's a replica, as we don't
    # make any changes to the database itself
    create_command += " --async"
  end

  # Create the sql instance with the specified/default parameters
  if gcloud(create_command, context: context, format: :boolean)
    async_additional =
      unless replica_for.nil?
        ". Database is still being created and may take some time to be available."
      end

    puts "Successfully created sql instance #{name}#{async_additional}"
  else
    puts "Failed to create sql instance"
    exit(1)
  end
end
set_secrets() click to toggle source
# File lib/seira/db/create.rb, line 179
def set_secrets
  env_name = name.tr('-', '_').upcase

  # If setting as primary, update relevant secrets. Only primaries have root passwords.
  if replica_for.nil?
    create_pgbouncer_secret(db_user: 'proxyuser', db_password: proxyuser_password)
    Secrets.new(app: app, action: 'set', args: ["#{env_name}_ROOT_PASSWORD=#{root_password}"], context: context).run
    # Set DATABASE_URL if not already set
    write_database_env(key: "DATABASE_URL", db_user: 'proxyuser', db_password: proxyuser_password) if Helpers.get_secret(context: context, key: "DATABASE_URL").nil?
    write_database_env(key: "#{env_name}_DB_URL", db_user: 'proxyuser', db_password: proxyuser_password)
  else
    # When creating a replica, we cannot manage users on the replica. We must manage the users on the primary, which the replica
    # inherits. For now we will use the same credentials that the primary uses.
    primary_uri = URI.parse(Helpers.get_secret(context: context, key: 'DATABASE_URL'))
    primary_user = primary_uri.user
    primary_password = primary_uri.password
    create_pgbouncer_secret(db_user: primary_user, db_password: primary_password)
    write_database_env(key: "#{env_name}_DB_URL", db_user: primary_user, db_password: primary_password)
  end
end
update_root_password() click to toggle source
# File lib/seira/db/create.rb, line 155
def update_root_password
  # Set the root user's password to something secure
  @root_password = SecureRandom.urlsafe_base64(32)

  if gcloud("sql users set-password postgres --instance=#{name} --password=#{root_password}", context: context, format: :boolean)
    puts "Set root password to #{root_password}"
  else
    puts "Failed to set root password"
    exit(1)
  end
end
write_database_env(key:, db_user:, db_password:) click to toggle source
# File lib/seira/db/create.rb, line 204
def write_database_env(key:, db_user:, db_password:)
  Secrets.new(app: app, action: 'set', args: ["#{key}=postgres://#{db_user}:#{db_password}@#{pgbouncer_service_name}:6432"], context: context).run
end