shiplane 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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