kaiser 0.0.0 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ module Cmds
5
+ class DbSave < Cli
6
+ def usage
7
+ <<~EOS
8
+ Shuts down the database docker container, backs up the database and brings the container back up.
9
+
10
+ The database will be saved as a tarball to \`~/.kaiser/<ENV_NAME>/<current_github_branch_name>/<DB_BACKUP_FILENAME>.tar.bz\`
11
+
12
+ Alternatively you can also save it to your current directory.
13
+
14
+ USAGE: kaiser db_save DB_BACKUP_FILENAME
15
+ kaiser db_save ./my_database.tar.bz
16
+ EOS
17
+ end
18
+
19
+ def execute(_opts)
20
+ ensure_setup
21
+ name = ARGV.shift || '.default'
22
+ save_db(name)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ module Cmds
5
+ class Deinit < Cli
6
+ def usage
7
+ <<~EOS
8
+ Removes the Kaiser environment from \`~/.kaiser/.config.yml\`. This however does not delete the \`~/.kaiser/databases/<ENV_NAME>\` directory.
9
+
10
+ USAGE: kaiser init ENV_NAME
11
+ EOS
12
+ end
13
+
14
+ def execute(_opts)
15
+ down
16
+ Config.config[:envs].delete(envname)
17
+ Config.config[:envnames].delete(Config.work_dir)
18
+ save_config
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ module Cmds
5
+ class Down < Cli
6
+ def usage
7
+ <<~EOS
8
+ Shuts down and *deletes* the containers that were started using \`kaiser up\`.
9
+
10
+ USAGE: kaiser down
11
+ EOS
12
+ end
13
+
14
+ def execute(_opts)
15
+ down
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ module Cmds
5
+ class Init < Cli
6
+ # TODO: Add explanation for the Already initialized error.
7
+ def usage
8
+ <<~EOS
9
+ Initializes a Kaiser environment and assigns ports for it in \`~/.kaiser/.config.yml\`. When running \`kaiser up\` later the directory \`~/.kaiser/databases/<ENV_NAME>\` will get created.
10
+
11
+ USAGE: kaiser init ENV_NAME
12
+ EOS
13
+ end
14
+
15
+ def execute(_opts)
16
+ return Optimist.die "Already initialized as #{envname}" if envname
17
+
18
+ name = ARGV.shift
19
+ return Optimist.die 'Needs environment name' if name.nil?
20
+
21
+ init_config_for_env(name)
22
+ save_config
23
+ end
24
+
25
+ def init_config_for_env(name)
26
+ Config.config[:envnames][Config.work_dir] = name
27
+ Config.config[:envs][name] = {
28
+ app_port: (largest_port + 1).to_s,
29
+ db_port: (largest_port + 2).to_s
30
+ }
31
+ Config.config[:largest_port] = Config.config[:largest_port] + 2
32
+ end
33
+
34
+ def largest_port
35
+ Config.config[:largest_port]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ module Cmds
5
+ class Login < Cli
6
+ def usage
7
+ <<~EOS
8
+ Executes a command on the application docker container. By executing the command \`sh\` you can get a login shell.
9
+
10
+ USAGE: kaiser login COMMAND
11
+ EOS
12
+ end
13
+
14
+ def execute(_opts)
15
+ ensure_setup
16
+ cmd = (ARGV || []).join(' ')
17
+ exec "docker exec -ti #{app_container_name} #{cmd}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ module Cmds
5
+ class Logs < Cli
6
+ def usage
7
+ <<~EOS
8
+ Continuously monitors the application container's logs.
9
+
10
+ USAGE: kaiser logs
11
+ EOS
12
+ end
13
+
14
+ def execute(_opts)
15
+ exec "docker logs -f #{app_container_name}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ module Cmds
5
+ class Root < Cli
6
+ def usage
7
+ <<~USAGE
8
+ Executes a command on the application docker container as a root user.
9
+
10
+ USAGE: kaiser root COMMAND
11
+ USAGE
12
+ end
13
+
14
+ def execute(_opts)
15
+ ensure_setup
16
+ cmd = (ARGV || []).join(' ')
17
+ exec "docker exec -ti --user root #{app_container_name} #{cmd}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ module Cmds
5
+ class Set < Cli
6
+ def usage
7
+ <<~EOS
8
+ This command lets you set up special variables that configure kaiser's behavior for you.
9
+
10
+ Available subcommands:
11
+
12
+ http-suffix - Sets the domain suffix for the reverse proxy to use (defaults to lvh.me)
13
+ cert-url - Sets up a URL from which HTTPS certificates can be downloaded.
14
+ cert-folder - Sets up a folder from which HTTPS certificates can be copied.
15
+ help-https - Shows the HTTPS notes.
16
+
17
+ USAGE: kaiser set cert-url
18
+ kaiser set cert-folder
19
+ kaiser set http-suffix
20
+ kaiser set help-https
21
+ EOS
22
+ end
23
+
24
+ def execute(_opts)
25
+ cmd = ARGV.shift
26
+ if cmd == 'cert-url'
27
+ Config.config[:cert_source] = {
28
+ url: ARGV.shift
29
+ }
30
+ elsif cmd == 'cert-folder'
31
+ Config.config[:cert_source] = {
32
+ folder: ARGV.shift
33
+ }
34
+ elsif cmd == 'http-suffix'
35
+ Config.config[:http_suffix] = ARGV.shift
36
+ elsif cmd == 'help-https'
37
+ puts <<~SET_HELP
38
+ Notes on HTTPS:
39
+
40
+ You need to set suffix and either cert-url or cert-folder to enable HTTPS.
41
+
42
+ cert-url and cert-folder are mutually exclusive. If you set one of them the other will be erased.
43
+
44
+ The cert-url and cert-folder must satisfy the following requirements to work:
45
+
46
+ The strings must be the root of certificates named after the suffix. For example,
47
+
48
+ if cert-url is https://mydomain.com/certs and your suffix is local.mydomain.com, the following
49
+ url need to be the certificate files:
50
+
51
+ https://mydomain.com/certs/local.mydomain.com.chain.pem
52
+ https://mydomain.com/certs/local.mydomain.com.crt
53
+ https://mydomain.com/certs/local.mydomain.com.key
54
+
55
+ Another example:
56
+
57
+ If you use suffix of localme.com and cert-folder is /home/me/https, The following files need to exist:
58
+
59
+ /home/me/https/localme.com.chain.pem
60
+ /home/me/https/localme.com.crt
61
+ /home/me/https/localme.com.key
62
+ SET_HELP
63
+ else
64
+ Optimist.die "Unknown subcommand: '#{cmd}'"
65
+ end
66
+ save_config
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ module Cmds
5
+ class Show < Cli
6
+ def usage
7
+ <<~EOS
8
+ Subcommand that shows information about the environment such as the TCP ports or the certificate used for HTTPS.
9
+
10
+ USAGE: kaiser show ports
11
+ kaiser show cert-source
12
+ kaiser show http-suffix
13
+ EOS
14
+ end
15
+
16
+ def execute(_opts)
17
+ ensure_setup
18
+ cmd = ARGV.shift
19
+ valid_cmds = 'ports cert-source http-suffix'
20
+ return Optimist.die "Available things to show: #{valid_cmds}" unless cmd
21
+
22
+ if cmd == 'ports'
23
+ Config.info_out.puts "app: #{app_port}"
24
+ Config.info_out.puts "db: #{db_port}"
25
+ elsif cmd == 'cert-source'
26
+ unless Config.config[:cert_source]
27
+ Optimist.die 'No certificate source set.
28
+ see kaiser set help'
29
+ end
30
+
31
+ source = Config.config[:cert_source][:url] || Config.config[:cert_source][:folder]
32
+ Config.info_out.puts source
33
+ elsif cmd == 'http-suffix'
34
+ Config.info_out.puts http_suffix
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ module Cmds
5
+ class Shutdown < Cli
6
+ def usage
7
+ # TODO: Explain a bit more about what these containers do and what shutting
8
+ # them down really means for an end user.
9
+ <<~EOS
10
+ Shuts down all the containers used internally by Kaiser.
11
+
12
+ USAGE: kaiser shutdown
13
+ EOS
14
+ end
15
+
16
+ def execute(_opts)
17
+ Config.config[:shared_names].each do |_, container_name|
18
+ killrm container_name
19
+ end
20
+
21
+ CommandRunner.run Config.out, "docker network rm #{Config.config[:networkname]}"
22
+ CommandRunner.run Config.out, "docker volume rm #{Config.config[:shared_names][:certs]}"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ module Cmds
5
+ class Up < Cli
6
+ option :attach, "Bind mount the current source code directory in the app container (as the \`kaiser attach\` command would)", short: '-a'
7
+
8
+ def usage
9
+ <<~EOS
10
+ Boots up the application in docker as defined in the \`Kaiserfile\` in its source code. Usually this will create two docker containers \`<ENV_NAME>-db\` and \`<ENV_NAME>-app\` running your database and application respectively.
11
+
12
+ A backup of the default database is created and saved to \`~/.kaiser/<ENV_NAME>/<current_github_branch_name>/.default.tar.bz\`. This can be restored at any time using the \`db_reset\` command.
13
+
14
+ USAGE: kaiser up
15
+ EOS
16
+ end
17
+
18
+ def execute(opts)
19
+ ensure_setup
20
+ setup_app
21
+ setup_db
22
+
23
+ if opts[:attach]
24
+ attach_app
25
+ else
26
+ start_app
27
+ end
28
+ end
29
+
30
+ def setup_app
31
+ Config.info_out.puts 'Setting up application'
32
+ File.write(tmp_dockerfile_name, docker_file_contents)
33
+ build_args = docker_build_args.map { |k, v| "--build-arg #{k}=#{v}" }
34
+ CommandRunner.run! Config.out, "docker build
35
+ -t kaiser:#{envname}-#{current_branch}
36
+ -f #{tmp_dockerfile_name} #{Config.work_dir}
37
+ #{build_args.join(' ')}"
38
+ FileUtils.rm(tmp_dockerfile_name)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
5
+ # This is the command runner
6
+ module Kaiser
7
+ # Make running easy
8
+ class CommandRunner
9
+ def self.run(out, cmd, &block)
10
+ out.puts "> #{cmd}"
11
+ CommandRunner.new(out, cmd).run_command(&block)
12
+ end
13
+
14
+ def self.run!(out, cmd, &block)
15
+ status = run(out, cmd, &block)
16
+ raise Kaiser::CmdError.new(cmd, status) if status.to_s != '0'
17
+ end
18
+
19
+ def initialize(out, cmd)
20
+ @out = out
21
+ @cmd = cmd.tr "\n", ' '
22
+ end
23
+
24
+ def print_and_return_status(status = 0)
25
+ @out.puts "$? = #{status}"
26
+ @out.flush
27
+ status
28
+ end
29
+
30
+ def print_lines(lines)
31
+ lines.each do |line|
32
+ @out.print line
33
+ @out.flush
34
+ yield line.chomp if block_given?
35
+ end
36
+ rescue Errno::EIO
37
+ # Happens when `lines` stream is closed
38
+ end
39
+
40
+ def run_command(&block)
41
+ PTY.spawn("#{@cmd} 2>&1") do |stdout, _stdin, pid|
42
+ print_lines(stdout, &block)
43
+ Process.wait(pid)
44
+ end
45
+ print_and_return_status $CHILD_STATUS.exitstatus
46
+ rescue PTY::ChildExited => e
47
+ print_and_return_status(e.status)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ class Config
5
+ class << self
6
+ attr_reader :work_dir,
7
+ :config_dir,
8
+ :config_file,
9
+ :kaiserfile,
10
+ :config,
11
+ :out,
12
+ :info_out
13
+
14
+ attr_writer :out,
15
+ :info_out
16
+
17
+ def load(work_dir)
18
+ @work_dir = work_dir
19
+ @config_dir = "#{ENV['HOME']}/.kaiser"
20
+
21
+ FileUtils.mkdir_p @config_dir
22
+ @config_file = "#{@config_dir}/.config.yml"
23
+ @kaiserfile = Kaiserfile.new("#{@work_dir}/Kaiserfile")
24
+
25
+ @config = {
26
+ envnames: {},
27
+ envs: {},
28
+ networkname: 'kaiser_net',
29
+ shared_names: {
30
+ redis: 'kaiser-redis',
31
+ nginx: 'kaiser-nginx',
32
+ chrome: 'kaiser-chrome',
33
+ dns: 'kaiser-dns',
34
+ certs: 'kaiser-certs'
35
+ },
36
+ largest_port: 9000,
37
+ always_verbose: false
38
+ }
39
+
40
+ load_config
41
+
42
+ alt_kaiserfile = "#{ENV['HOME']}/kaiserfiles/Kaiserfile.#{@config[:envnames][work_dir]}"
43
+ @kaiserfile = Kaiserfile.new(alt_kaiserfile) if File.exist?(alt_kaiserfile)
44
+
45
+ @config
46
+ end
47
+
48
+ def always_verbose?
49
+ @config[:always_verbose]
50
+ end
51
+
52
+ def load_config
53
+ loaded = YAML.load_file(@config_file) if File.exist?(@config_file)
54
+
55
+ config_shared_names = @config[:shared_names] if @config
56
+ loaded_shared_names = loaded[:shared_names] if loaded
57
+
58
+ @config = {
59
+ **(@config || {}),
60
+ **(loaded || {}),
61
+ shared_names: { **(config_shared_names || {}), **(loaded_shared_names || {}) }
62
+ }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ # Docker control
5
+ class DockerControl
6
+ def initialize
7
+ @docker
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ # Prints dots for every line printed
5
+ class Dotter
6
+ attr_accessor :dotted
7
+
8
+ def method_missing(name, *value)
9
+ $stderr.print '.'
10
+ @dotted = true
11
+ super unless @dotted
12
+ end
13
+
14
+ def respond_to_missing?(name)
15
+ super
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ # Base class for Kaiser-related errors.
5
+ class Error < StandardError
6
+ end
7
+
8
+ # Raised when a command exits with non-zero exit status.
9
+ class CmdError < Error
10
+ def initialize(cmd, status)
11
+ super "ERROR\n#{cmd}\n- exited with code #{status}"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ # This class is responsible for parsing the Kaiserfile
5
+ class Kaiserfile
6
+ attr_accessor :docker_file_contents,
7
+ :docker_build_args,
8
+ :database,
9
+ :port,
10
+ :database_reset_command,
11
+ :attach_mounts,
12
+ :server_type
13
+
14
+ def initialize(filename)
15
+ Optimist.die 'No Kaiserfile in current directory' unless File.exist? filename
16
+
17
+ @databases = {}
18
+ @attach_mounts = []
19
+ @params_array = []
20
+ @server_type = :unknown
21
+
22
+ instance_eval File.read(filename), filename
23
+ end
24
+
25
+ def plugin(name)
26
+ raise "Plugin #{name} is not loaded." unless Plugin.loaded?(name)
27
+
28
+ Plugin.all_plugins[name].new(self).on_init
29
+ end
30
+
31
+ def dockerfile(name, options = {})
32
+ @docker_file_contents = File.read(name)
33
+ @docker_build_args = options[:args] || {}
34
+ end
35
+
36
+ def attach_mount(from, to)
37
+ attach_mounts << [from, to]
38
+ end
39
+
40
+ def db(image,
41
+ data_dir:,
42
+ port:,
43
+ params: '',
44
+ commands: '',
45
+ waitscript: nil,
46
+ waitscript_params: '')
47
+ @database = {
48
+ image: image,
49
+ port: port,
50
+ data_dir: data_dir,
51
+ params: params,
52
+ commands: commands,
53
+ waitscript: waitscript,
54
+ waitscript_params: waitscript_params
55
+ }
56
+ end
57
+
58
+ def expose(port)
59
+ @port = port
60
+ end
61
+
62
+ def app_params(value)
63
+ @params_array << value
64
+ end
65
+
66
+ def params
67
+ @params_array.join(' ')
68
+ end
69
+
70
+ def db_reset_command(value)
71
+ @database_reset_command = value
72
+ end
73
+
74
+ def type(value)
75
+ raise 'Valid server types are: [:http]' if value != :http
76
+
77
+ @server_type = value
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ # To implement a Kaiser plugin you must inherit from this class.
5
+ # For example,
6
+ #
7
+ # class MyPlugin < Plugin
8
+ # def on_init
9
+ # puts 'My plugin is loaded!'
10
+ # end
11
+ # end
12
+ #
13
+ # Then in your Kasierfile
14
+ #
15
+ # plugin :my_plugin
16
+ #
17
+ # Plugins has access the Kaiserfile DSL. For example,
18
+ #
19
+ # class Ruby < Plugin
20
+ # def on_init
21
+ # attach_mount 'Gemfile', '/usr/app/Gemfile'
22
+ # attach_mount 'Gemfile.lock', '/usr/app/Gemfile.lock'
23
+ # end
24
+ # end
25
+ #
26
+ class Plugin
27
+ def initialize(kaiserfile)
28
+ @kaiserfile = kaiserfile
29
+ end
30
+
31
+ def self.loaded?(name)
32
+ Plugin.all_plugins.key?(name)
33
+ end
34
+
35
+ def self.inherited(plugin)
36
+ # underscore class name
37
+ name = plugin.to_s.split('::').last
38
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
39
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
40
+ .gsub(/([a-z])(\d)/, '\1_\2')
41
+ .tr('-', '_').downcase
42
+
43
+ Plugin.all_plugins[name.to_sym] = plugin
44
+ end
45
+
46
+ def self.all_plugins
47
+ @all_plugins ||= {}
48
+ end
49
+
50
+ def on_init
51
+ raise 'Please implement #on_init'
52
+ end
53
+
54
+ def method_missing(method_sym, *arguments, &block) # rubocop:disable all
55
+ @kaiserfile.send(method_sym, *arguments, &block)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ class GitSubmodule < Plugin
5
+ def on_init
6
+ `git submodule status`.lines.each do |line|
7
+ # The git-submodule man page says uninitialized submodules are prefixed with a -
8
+ # but I found this unreliable. While testing I pressed Control-C in the middle of
9
+ # the update command so some submodule would be initialized and others wouldn't.
10
+ # After that, the status command had removed the - for every submodule.
11
+ # Therefore we just check if there's files in the directory instead.
12
+ dir = line.strip.split(' ')[1]
13
+ if !Dir.exist?(dir) || Dir.empty?(dir) # rubocop:disable Style/Next
14
+ puts "Found uninitialized git submodule '#{dir}'"
15
+ system 'git submodule update --init --recursive'
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaiser
4
+ VERSION = '0.4.5'
5
+ end