Islo w/basic MySQL, Redis and PostgreSQL support

This commit is contained in:
Loic Nageleisen 2014-05-14 18:51:59 +02:00
commit 743d628733
10 changed files with 491 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.gem
Gemfile.lock

3
Gemfile Normal file
View file

@ -0,0 +1,3 @@
source 'https://rubygems.org'
gemspec

20
LICENSE Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2014 Loic Nageleisen
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

74
README.md Normal file
View file

@ -0,0 +1,74 @@
# Islo - Self-contained apps
Make apps completely self-contained by abstracting service process settings and execution.
## Quick, show me how to use my favorite daemon!
First, install Islo:
```
$ gem install islo
```
Then, a nice example might be worth a thousand words, so here goes:
### MySQL or MariaDB
```
$ islo mysql_install_db # creates database in db/mysql
$ islo mysqld # starts server without daemonizing[^1]
$ islo mysql # connects to running server via unix socket in tmp/sockets
```
### Redis
```
$ islo redis-init # creates directory in db/redis
$ islo redis-server # starts server without daemonizing[^1]
$ islo redis-cli # connects to server via unix socket in tmp/sockets
```
### PostgreSQL
```
$ islo initdb # creates directory in db/postgres
$ islo postgres # starts server without daemonizing[^1]
$ islo psql # connects to server via unix socket in tmp/sockets
```
[^1]: Best used in a [Procfile](https://github.com/ddollar/foreman)
## What's more to know?
- Additional arguments are passed to the command.
Run `islo --help` for details.
- Servers will listen only on unix sockets, TCP will be disabled.
This saves headaches when you have to handle multiple projects, and thus
conflicting ports. Also, it's too easy to forget not to listen for the world.
## My service is installed in a non-standard location/I want to use different versions in different projects
Configuration is a pending item, which will make locations selectable.
## I don't like how it assumes a Rails project layout
Configuration is a pending item, which will help set relevant paths.
## I've got a super service you don't seem to know about
Some configuration may help you soon. Also, contributions are welcome.
## I can't be bothered/always forget to type *islo* before my commands every single time!
Look soon enough under `support` for a few optional helpers for your favorite shell.
## I want to contribute. How?
Great! Write specs, have them all pass, respect rubocop, rebase on master and make your PR.
## License
MIT, see [LICENSE](LICENSE).

0
Rakefile Normal file
View file

5
bin/islo Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env ruby
require 'islo/cli'
Islo::CLI.start

29
islo.gemspec Normal file
View file

@ -0,0 +1,29 @@
# -*- encoding: utf-8 -*-
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
require 'islo/version'
Gem::Specification.new do |s|
s.name = 'islo'
s.version = Islo::VERSION
s.authors = ['Loic Nageleisen']
s.email = ['loic.nageleisen@gmail.com']
s.homepage = 'http://github.com/lloeki/islo'
s.summary = %q(Self-contained apps)
s.description = <<-EOT
Makes app completely self-contained by abstracting
service process settings and execution
EOT
s.license = 'MIT'
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n")
.map { |f| File.basename(f) }
s.require_paths = ['lib']
s.add_dependency 'rainbow', '~> 2.0'
s.add_dependency 'slop'
s.add_development_dependency 'rspec', '~> 2.14'
s.add_development_dependency 'rake', '~> 10.3'
s.add_development_dependency 'pry'
end

264
lib/islo.rb Normal file
View file

@ -0,0 +1,264 @@
# Islo - application isolator
module Islo
# Generic command execution
class Command
class Error < StandardError; end
attr_reader :title, :wd
attr_reader :command, :args
def initialize(args, title: nil, wd: nil)
@command = args.shift
@args = args
@title = title unless title.nil? || title.empty?
@wd = Pathname.new(wd || Dir.pwd)
end
def title?
!title.nil?
end
def exec
Dir.chdir(wd.to_s)
Kernel.exec(*args_for_exec)
rescue SystemCallError => e
raise Command::Error, e.message
end
private
def args_for_exec
command = (title? ? [@command, title] : @command)
args = self.args
[command] + args
end
end
class << self
def commands
@commands ||= {}
end
def register(command)
commands[command.name.to_sym] = command
end
def command(args, options = {})
name = File.basename(args[0]).to_sym
(commands[name] || Command).new(args, options)
end
end
# MySQL support
module Mysql
# MySQL client
class Client < Command
def self.name
:mysql
end
def args
%W(
--socket=#{wd}/tmp/sockets/mysql.sock
-uroot
) + super
end
Islo.register(self)
end
# MySQL server
class Server < Command
def self.name
:mysqld
end
def args
%W(
--no-defaults
--datadir=#{wd}/db/mysql
--pid-file=#{wd}/tmp/pids/mysqld.pid
--socket=#{wd}/tmp/sockets/mysql.sock
--skip-networking
) + super
end
Islo.register(self)
end
# MySQL initializer
class Init < Command
def self.name
:mysql_install_db
end
def args
%W(
--no-defaults
--basedir=#{Mysql.basedir}
--datadir=#{wd}/db/mysql
--pid-file=#{wd}/tmp/pids/mysqld.pid
) + super
end
Islo.register(self)
end
def self.basedir
'/usr/local'
end
end
# Redis support
module Redis
# Redis client
class Client < Command
def self.name
:'redis-cli'
end
def args
%w(-s redis.sock)
end
# Change working directory (makes for a nicer prompt)
def wd
super + 'tmp/sockets'
end
Islo.register(self)
end
# Redis server
class Server < Command
def self.name
:'redis-server'
end
def args
%W(#{wd}/db/redis/redis.conf)
end
Islo.register(self)
end
# Redis initializer
#
# Creates a minimal configuration because redis-server doesn't accept
# arguments allowing for paths to be set.
class Init < Command
def self.name
:'redis-init'
end
def exec
FileUtils.mkdir_p(wd + 'db/redis')
File.open(wd + 'db/redis/redis.conf', 'w') do |f|
f << template.gsub('${WORKING_DIR}', wd.to_s)
end
rescue SystemCallError => e
raise Command::Error, e.message
end
private
# rubocop:disable MethodLength,LineLength
def template
<<-EOT.gsub(/^ +/, '')
daemonize no
pidfile ${WORKING_DIR}/pids/redis.pid
port 0
bind 127.0.0.1
unixsocket ${WORKING_DIR}/tmp/sockets/redis.sock
unixsocketperm 700
timeout 0
tcp-keepalive 0
loglevel notice
databases 1
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ${WORKING_DIR}/db/redis
slave-serve-stale-data yes
slave-read-only yes
repl-disable-tcp-nodelay no
slave-priority 100
appendonly yes
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
EOT
end
Islo.register(self)
end
end
# PostgreSQL support
module Postgres
# PostgreSQL client
class Client < Command
def self.name
:psql
end
def args
%W(--host=#{wd}/tmp/sockets) + super
end
Islo.register(self)
end
# PostgreSQL server
class Server < Command
def self.name
:postgres
end
def args
%W(
-D #{wd}/db/postgres
-k #{wd}/tmp/sockets
) + ['-h', ''] + super
end
Islo.register(self)
end
# PostgreSQL initializer
class Init < Command
def self.name
:initdb
end
def args
%W(
-D #{wd}/db/postgres
) + super
end
Islo.register(self)
end
end
end

91
lib/islo/cli.rb Normal file
View file

@ -0,0 +1,91 @@
require 'slop'
require 'islo'
require 'rainbow/ext/string'
module Islo
# Handle and run the command line interface
class CLI
NAME = File.basename($PROGRAM_NAME)
# See sysexits(3)
RC = { ok: 0,
error: 1,
usage: 64,
data: 65,
noinput: 66,
nouser: 67,
nohost: 68,
unavailable: 69,
software: 70,
oserr: 71,
osfile: 72,
cantcreat: 73,
ioerr: 74,
tempfail: 75,
protocol: 76,
noperm: 77,
config: 78 }
# Shortcut for Islo::CLI.new.start
def self.start
new.start
end
attr_reader :args
def opts
@opts.to_hash
end
def initialize(args = ARGV)
@args, @opts = parse(args)
rescue Slop::InvalidOptionError => e
die(:usage, e.message)
rescue Slop::MissingArgumentError => e
die(:usage, e.message)
end
# Start execution based on options
def start
Islo.command(args, title: opts[:title]).exec
rescue Islo::Command::Error => e
die(:oserr, e.message)
end
private
# Stop with a popping message and exit with a return code
def die(rc, message)
$stderr.puts('Error: '.color(:red) << message)
exit(rc.is_a?(Symbol) ? RC[rc] : rc)
end
# Stop with a nice message
def bye(message)
$stdout.puts message
exit
end
def version_string
"#{NAME} v#{Islo::VERSION}"
end
# Parse arguments and set options
# rubocop:disable MethodLength
def parse(args)
args = args.dup
opts = Slop.parse!(args, strict: true, help: true) do
banner "Usage: #{NAME} [options] [--] command arguments"
on 't', 'title=', 'String displayed in process list', argument: true
on 'd', 'directory=', 'Change working directory', argument: true
on 'v', 'version', 'Print the version', -> { bye(version_string) }
end
fail Slop::MissingArgumentError, 'missing an argument' if args.empty?
[args, opts]
end
end
end

3
lib/islo/version.rb Normal file
View file

@ -0,0 +1,3 @@
module Islo
VERSION = '0.1.0'
end