shiplane 0.1.1

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,121 @@
1
+ require 'fileutils'
2
+
3
+ module Shiplane
4
+ class CheckoutArtifact
5
+ extend Forwardable
6
+ attr_accessor :sha
7
+
8
+ delegate %i(build_config project_config) => :shiplane_config
9
+
10
+ def initialize(sha)
11
+ @sha = sha
12
+
13
+ # call this before changing directories.
14
+ # This prevents race conditions where the config file is accessed before being downloaded
15
+ shiplane_config
16
+ end
17
+
18
+ def appname
19
+ @appname ||= project_config['appname']
20
+ end
21
+
22
+ def shiplane_config
23
+ @shiplane_config ||= Shiplane::Configuration.new
24
+ end
25
+
26
+ def github_token
27
+ @github_token ||= ENV['GITHUB_TOKEN']
28
+ end
29
+
30
+ def git_url
31
+ "https://#{github_token ? "#{github_token}@" : ''}github.com/#{project_config['origin']}"
32
+ end
33
+
34
+ def app_directory
35
+ @app_directory ||= File.join(Dir.pwd, 'docker_builds', appname)
36
+ end
37
+
38
+ def build_directory
39
+ @build_directory ||= File.join(app_directory, "#{appname}-#{sha}")
40
+ end
41
+
42
+ def make_directory
43
+ FileUtils.mkdir_p build_directory
44
+ end
45
+
46
+ def checkout!
47
+ return if File.exist?(File.join(build_directory, Shiplane::SHIPLANE_CONFIG_FILENAME))
48
+
49
+ puts "Checking out Application #{appname}[#{sha}]..."
50
+ make_directory
51
+
52
+ success = true
53
+ FileUtils.cd app_directory do
54
+ success = success && system("echo 'Downloading #{git_url}/archive/#{sha}.tar.gz --output #{appname}-#{sha}.tar.gz'")
55
+ success = success && system("curl -L #{git_url}/archive/#{sha}.tar.gz --output #{appname}-#{sha}.tar.gz")
56
+ success = success && system("tar -xzf #{appname}-#{sha}.tar.gz -C .")
57
+ end
58
+
59
+ raise "Errors encountered while downloading archive" unless success
60
+ puts "Finished checking out Application"
61
+ tasks.each(&method(:send))
62
+ end
63
+
64
+ def tasks
65
+ [:make_directories, :copy_env_files, :copy_insert_on_build_files, :unignore_required_directories]
66
+ end
67
+
68
+ def make_directories
69
+ FileUtils.cd build_directory do
70
+ required_directories.each do |directory|
71
+ FileUtils.mkdir_p directory
72
+ end
73
+ end
74
+ end
75
+
76
+ def copy_env_files
77
+ puts "Copying in environment files..."
78
+ FileUtils.cp File.join(Dir.pwd, build_config.fetch('environment_file', '.env')), File.join(build_directory, '.env')
79
+ puts "Environment Files Copied"
80
+ end
81
+
82
+ def copy_insert_on_build_files
83
+ puts "Copying application configuration files..."
84
+
85
+ if Dir.exist? File.join(build_config.fetch('settings_folder', '.shiplane'), "insert_on_build")
86
+ FileUtils.cd File.join(build_config.fetch('settings_folder', '.shiplane'), "insert_on_build") do
87
+ Dir["*/**"].each do |filepath|
88
+ if File.extname(filepath) == ".erb"
89
+ copy_erb_file(filepath)
90
+ else
91
+ FileUtils.cp filepath, File.join(build_directory, filepath)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ puts "Configuration Files Copied"
97
+ end
98
+
99
+ def copy_erb_file(filepath)
100
+ File.write(File.join(build_directory, filepath.gsub(".erb","")), ERB.new(File.read(filepath)).result, mode: 'w')
101
+ end
102
+
103
+ def unignore_required_directories
104
+ puts "Adding Required Directories as explicit inclusions in ignore file..."
105
+ File.open(File.join(build_directory, '.dockerignore'), 'a') do |file|
106
+ required_directories.each do |directory|
107
+ file.puts "!#{directory}/*"
108
+ end
109
+ end
110
+ puts "Finished including required directories..."
111
+ end
112
+
113
+ def required_directories
114
+ ['vendor']
115
+ end
116
+
117
+ def self.checkout!(sha)
118
+ new(sha).checkout!
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,38 @@
1
+ require_relative 'extensions'
2
+
3
+ module Shiplane
4
+ class ComposeHash
5
+ attr_accessor :compose_file, :production_config
6
+
7
+ def initialize(compose_file, production_config)
8
+ @compose_file = compose_file
9
+ @production_config = production_config
10
+ end
11
+
12
+ def production_yml
13
+ blacklisted_nodes.inject(whitelisted_hash){ |acc, node| acc.blacklist(node) }
14
+ end
15
+
16
+ def compose_hash
17
+ @compose_hash ||= YAML.load(compose_file)
18
+ end
19
+
20
+ def whitelisted_hash
21
+ @whitelisted_hash ||= compose_hash.whitelist(*default_whitelisted_nodes, *whitelisted_nodes)
22
+ end
23
+
24
+ def blacklisted_nodes
25
+ @blacklisted_nodes ||= production_config.fetch('blacklist', [])
26
+ end
27
+
28
+ def whitelisted_nodes
29
+ @whitelisted_nodes ||= production_config.fetch('whitelist', [])
30
+ end
31
+
32
+ def default_whitelisted_nodes
33
+ [
34
+ "version",
35
+ ]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ module Shiplane
2
+ class Configuration
3
+ attr_accessor :project_folder
4
+
5
+ def initialize(project_folder = nil)
6
+ @project_folder = project_folder || Dir.pwd
7
+ end
8
+
9
+ def shiplane_config_file
10
+ @shiplane_config_file ||= File.join(project_folder, Shiplane::SHIPLANE_CONFIG_FILENAME)
11
+ end
12
+
13
+ def config
14
+ @config ||= YAML.load_file(shiplane_config_file)
15
+ end
16
+
17
+ def build_config
18
+ @build_config ||= config.fetch('build', {})
19
+ end
20
+
21
+ def bootstrap_config
22
+ @bootstrap_config ||= config.fetch('bootstrap', {})
23
+ end
24
+
25
+ def deploy_config
26
+ @deploy_config ||= config.fetch('deploy', {})
27
+ end
28
+
29
+ def project_config
30
+ @project_config ||= config.fetch('project', {})
31
+ end
32
+
33
+ def self.config(project_folder = nil)
34
+ new(project_folder).config
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,59 @@
1
+ require 'facets/hash/traverse'
2
+ require_relative 'configuration'
3
+ require_relative 'compose_hash'
4
+
5
+ module Shiplane
6
+ class ConvertComposeFile
7
+ extend Forwardable
8
+ attr_accessor :project_folder, :sha
9
+
10
+ delegate %i(build_config) => :shiplane_config
11
+
12
+ def initialize(project_folder, sha)
13
+ @project_folder = project_folder
14
+ @sha = sha
15
+ end
16
+
17
+ def shiplane_config
18
+ @shiplane_config ||= Shiplane::Configuration.new
19
+ end
20
+
21
+ def compose_config
22
+ @compose_config ||= build_config.fetch('compose', {})
23
+ end
24
+
25
+ def compose_filepath
26
+ @compose_filepath ||= File.join(project_folder, build_config.fetch('compose_filepath', Shiplane::DEFAULT_COMPOSEFILE_FILEPATH))
27
+ end
28
+
29
+ def converted_compose_hash
30
+ @converted_compose_hash ||= Shiplane::ComposeHash.new(File.new(compose_filepath), compose_config).production_yml
31
+ end
32
+
33
+ def converted_output
34
+ @converted_output ||= converted_compose_hash.dup.tap do |hash|
35
+ build_config.fetch('artifacts', {}).each do |(appname, config)|
36
+ hash.deep_merge!({ 'services' => { appname => { 'image' => "#{config['repo']}:#{sha}" } } })
37
+ end
38
+
39
+ hash.traverse! do |key, value|
40
+ if (key == 'env_file' && value == '.env.development')
41
+ [key, '.env.production']
42
+ else
43
+ [key, value]
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def convert_output!
50
+ puts "Converting Compose File..."
51
+ File.write(compose_filepath, converted_output.to_yaml)
52
+ puts "Compose File Converted..."
53
+ end
54
+
55
+ def self.convert_output!(project_folder, sha)
56
+ new(project_folder, sha).convert_output!
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,60 @@
1
+ require_relative 'configuration'
2
+
3
+ module Shiplane
4
+ class ConvertDockerfile
5
+ extend Forwardable
6
+ attr_accessor :artifact_context, :compose_context, :project_folder
7
+
8
+ delegate %i(build_config project_config) => :shiplane_config
9
+
10
+ def initialize(project_folder, artifact_context, compose_context)
11
+ @project_folder = project_folder
12
+ @artifact_context = artifact_context
13
+ @compose_context = compose_context
14
+ end
15
+
16
+ def appname
17
+ @appname ||= project_config['appname']
18
+ end
19
+
20
+ def shiplane_config
21
+ @shiplane_config ||= Shiplane::Configuration.new
22
+ end
23
+
24
+ def dockerfile_name
25
+ @dockerfile_name ||= compose_context.fetch('build', {}).fetch('context', '.').tap do |filename|
26
+ filename.gsub!(/^\.$/, 'Dockerfile')
27
+ end
28
+ end
29
+
30
+ def dockerfile_filepath
31
+ @dockerfile_filepath ||= File.join(project_folder, dockerfile_name)
32
+ end
33
+
34
+ def dockerfile_production_stages_filepath
35
+ @dockerfile_production_stages_filepath ||= File.join(Dir.pwd, build_config.fetch('settings_folder', '.shiplane'), Shiplane::DEFAULT_PRODUCTION_DOCKERFILE_STAGES_FILEPATH)
36
+ end
37
+
38
+ def entrypoint
39
+ @entrypoint ||= artifact_context.fetch('command', compose_context.fetch('command', "bin/rails s"))
40
+ end
41
+
42
+ def converted_output
43
+ @converted_output ||= [
44
+ File.read(dockerfile_filepath),
45
+ File.read(dockerfile_production_stages_filepath),
46
+ # "ENTRYPOINT #{entrypoint}",
47
+ ].join("\n\n")
48
+ end
49
+
50
+ def convert_output!
51
+ puts "Converting Dockerfile..."
52
+ File.write(dockerfile_filepath, converted_output)
53
+ puts "Dockerfile Converted..."
54
+ end
55
+
56
+ def self.convert_output!(project_folder, artifact_context, compose_context)
57
+ new(project_folder, artifact_context, compose_context).convert_output!
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,17 @@
1
+ module Shiplane
2
+ module Deploy
3
+ class Configuration
4
+ attr_accessor :env, :name, :options
5
+
6
+ def initialize(name, options, env)
7
+ @name = name
8
+ @options = options
9
+ @env = env
10
+ end
11
+
12
+ def docker_command(role)
13
+ role.requires_sudo? ? "sudo docker" : "docker"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,93 @@
1
+ require_relative 'configuration'
2
+
3
+ module Shiplane
4
+ module Deploy
5
+ class ContainerConfiguration < Configuration
6
+ def network_alias
7
+ @network_alias ||= options.fetch(:alias, container_name)
8
+ end
9
+
10
+ def volumes
11
+ @volumes ||= options.fetch(:volumes, [])
12
+ end
13
+
14
+ def published_ports
15
+ @published_ports ||= [options.fetch(:publish, [])].flatten
16
+ end
17
+
18
+ def exposed_ports
19
+ @exposed_ports ||= [options.fetch(:expose, [])].flatten - published_ports
20
+ rescue => e
21
+ binding.pry
22
+ end
23
+
24
+ def container_name
25
+ @container_name ||= "#{env.fetch(:application)}_#{name}_#{env.fetch(:sha)}"
26
+ end
27
+
28
+ def image_name
29
+ @image_name ||= "#{options.fetch(:repo)}:#{image_tag}"
30
+ end
31
+
32
+ def image_tag
33
+ @image_tag ||= options.fetch(:tag, "#{env.fetch(:stage)}-#{env.fetch(:sha)}")
34
+ end
35
+
36
+ def virtual_host
37
+ @virtual_host ||= options[:virtual_host]
38
+ end
39
+
40
+ def letsencrypt_host
41
+ @letsencrypt_host ||= options.fetch(:letsencrypt_host, virtual_host)
42
+ end
43
+
44
+ def letsencrypt_email
45
+ @letsencrypt_email ||= options[:letsencrypt_email]
46
+ end
47
+
48
+ def networks
49
+ @networks ||= options.fetch(:networks, [])
50
+ end
51
+
52
+ def startup_command
53
+ @startup_command ||= options[:command]
54
+ end
55
+
56
+ def network_connect_commands(role)
57
+ @network_commands ||= networks.map do |network|
58
+ [
59
+ docker_command(role),
60
+ "network connect",
61
+ "--alias #{network_alias}",
62
+ network,
63
+ container_name,
64
+ "|| true",
65
+ ].flatten.compact.join(" ")
66
+ end
67
+ end
68
+
69
+ def run_command(role)
70
+ @command ||= [
71
+ docker_command(role),
72
+ "run -d",
73
+ volumes.map{|volume_set| "-v #{volume_set}" },
74
+ published_ports.map{|port| "--expose #{port} -p #{port}" },
75
+ exposed_ports.map{|port| "--expose #{port}" },
76
+ "--name #{container_name}",
77
+ virtual_host ? "-e VIRTUAL_HOST=#{virtual_host}" : nil,
78
+ letsencrypt_host ? "-e LETSENCRYPT_HOST=#{letsencrypt_host}" : nil,
79
+ letsencrypt_email ? "-e LETSENCRYPT_EMAIL=#{letsencrypt_email}" : nil,
80
+ image_name,
81
+ startup_command ? startup_command : nil,
82
+ ].flatten.compact.join(" ")
83
+ end
84
+
85
+ def run_commands(role)
86
+ @run_commands ||= [
87
+ run_command(role),
88
+ network_connect_commands(role),
89
+ ].flatten
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,40 @@
1
+ require_relative 'configuration'
2
+
3
+ module Shiplane
4
+ module Deploy
5
+ class NetworkConfiguration < Configuration
6
+ def connections
7
+ @connections ||= options.fetch(:connections, [])
8
+ end
9
+
10
+ def connect_commands(role)
11
+ @connect_commands ||=
12
+ connections.map do |connection|
13
+ [
14
+ docker_command(role),
15
+ "network connect",
16
+ name,
17
+ connection,
18
+ "|| true",
19
+ ].flatten.compact.join(" ")
20
+ end
21
+ end
22
+
23
+ def create_command(role)
24
+ @create_command ||= [
25
+ docker_command(role),
26
+ "network create",
27
+ name,
28
+ "|| true",
29
+ ].flatten.compact.join(" ")
30
+ end
31
+
32
+ def create_commands(role)
33
+ [
34
+ create_command(role),
35
+ connect_commands(role),
36
+ ].flatten
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,49 @@
1
+ require 'facets/hash/deep_merge'
2
+
3
+ class Hash
4
+ def whitelist(*keymaps)
5
+ self.dup.whitelist!(*keymaps)
6
+ end
7
+
8
+ def whitelist!(*keymaps)
9
+ keymaps.map do |map|
10
+ deep_subset(map)
11
+ end.inject(&:deep_merge)
12
+ end
13
+
14
+ def deep_subset(keymap)
15
+ self.dup.deep_subset!(keymap)
16
+ end
17
+
18
+ def deep_subset!(keymap)
19
+ keypath_keys = keymap.split('.')
20
+ return {} unless keypath_keys.size >= 1
21
+
22
+ deepest_subset = { keypath_keys.last => dig(*keypath_keys) }
23
+
24
+ keypath_keys[0..-2].reverse.inject(deepest_subset)do |accum, key|
25
+ accum = { key => accum }
26
+ end
27
+ end
28
+
29
+ def blacklist(keymap)
30
+ self.dup.blacklist!(keymap)
31
+ end
32
+
33
+ def blacklist!(keymap, parentparts = nil)
34
+ keypart, *rest = keymap.split(".")
35
+ keychain = [parentparts, keypart].compact.join(".")
36
+
37
+ self.each do |k, v|
38
+ v.blacklist!(Array(rest).join("."), keychain) if v.is_a?(Hash) && k.to_s == keypart.to_s
39
+ self.delete(k) if k.to_s == keypart.to_s && rest.empty?
40
+ end
41
+ end
42
+ end
43
+
44
+ class Array
45
+ def pad(pad_length, character = nil)
46
+ return self if size >= pad_length
47
+ self + [character] * (pad_length - size)
48
+ end
49
+ end
@@ -0,0 +1,96 @@
1
+ require 'sshkit'
2
+ require 'sshkit/dsl'
3
+
4
+ module Shiplane
5
+ class Host
6
+ extend Forwardable
7
+ include SSHKit::DSL
8
+
9
+ attr_accessor :env, :host, :role
10
+ def_delegators :env, :servers
11
+ def_delegators :role, :hostname
12
+
13
+ SSHKIT_PROPERTIES = %i(user password keys hostname port ssh_options)
14
+
15
+ def initialize(role, env)
16
+ @role = role
17
+ @env = env
18
+ end
19
+
20
+ def host
21
+ @host ||= SSHKit::Host.new(sshkit_options)
22
+ end
23
+
24
+ def capistrano_role
25
+ @capistrano_role ||= role.dup.tap do |r|
26
+ r.properties.set(:ssh_options, ssh_options)
27
+ end
28
+ end
29
+
30
+ def sshkit_values
31
+ {
32
+ interaction_handler: { "[sudo] password for #{user}: " => "#{password}\n" }
33
+ }
34
+ end
35
+
36
+ def requires_sudo?
37
+ @requires_sudo ||= config.fetch('requires_sudo', false)
38
+ end
39
+
40
+ private
41
+
42
+ def user
43
+ ssh_options.fetch("user", "")
44
+ end
45
+
46
+ def password
47
+ ssh_options.fetch("password", "")
48
+ end
49
+
50
+ def sshkit_options
51
+ @sshkit_options ||= options.merge(hostname: hostname).slice(*SSHKIT_PROPERTIES)
52
+ end
53
+
54
+ def options
55
+ @options ||= role.properties.to_h.symbolize_keys.merge(ssh_options: ssh_options)
56
+ end
57
+
58
+ def ssh_options
59
+ @ssh_options ||= config.fetch('ssh_options', {}).symbolize_keys
60
+ end
61
+
62
+ def config
63
+ self.class.config.fetch('deploy', {}).fetch('servers', {}).fetch(hostname, {})
64
+ end
65
+
66
+ def with_context(&block)
67
+ set(:shiplane_sshkit_values, sshkit_values)
68
+ yield
69
+ set(:shiplane_sshkit_values, nil)
70
+ end
71
+
72
+ def sshkit_output
73
+ @sshkit_output ||= SSHKit.config.output
74
+ end
75
+
76
+ def write_message(verbosity, message)
77
+ sshkit_output.write(SSHKit::LogMessage.new(verbosity, message))
78
+ end
79
+
80
+ def self.env_file
81
+ config.fetch("bootstrap", {}).fetch('env_file', '.env')
82
+ end
83
+
84
+ def self.config
85
+ @config ||= YAML.load(File.read(config_filepath))
86
+ end
87
+
88
+ def self.config_filepath
89
+ File.join("shiplane.yml")
90
+ end
91
+
92
+ def self.bootstrap!(host, env)
93
+ new(host, env).bootstrap!
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,5 @@
1
+ class Shiplane::Railtie < Rails::Railtie
2
+ rake_tasks do
3
+ load 'shiplane/tasks/install.rake'
4
+ end
5
+ end