bookbindery 4.1.0 → 4.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.
- checksums.yaml +4 -4
- data/lib/bookbinder/cf_command_runner.rb +33 -30
- data/lib/bookbinder/cli.rb +15 -7
- data/lib/bookbinder/code_example_reader.rb +5 -7
- data/lib/bookbinder/commands/bind/directory_preparer.rb +1 -1
- data/lib/bookbinder/commands/build_and_push_tarball.rb +18 -6
- data/lib/bookbinder/commands/collection.rb +16 -21
- data/lib/bookbinder/commands/push_from_local.rb +27 -19
- data/lib/bookbinder/commands/push_to_prod.rb +30 -34
- data/lib/bookbinder/commands/tag.rb +5 -6
- data/lib/bookbinder/commands/update_local_doc_repos.rb +0 -1
- data/lib/bookbinder/config/cf_credentials.rb +1 -14
- data/lib/bookbinder/deploy/app_fetcher.rb +36 -0
- data/lib/bookbinder/deploy/archive.rb +102 -0
- data/lib/bookbinder/deploy/artifact.rb +26 -0
- data/lib/bookbinder/deploy/blue_green_app.rb +29 -0
- data/lib/bookbinder/deploy/cf_routes.rb +32 -0
- data/lib/bookbinder/deploy/deployment.rb +55 -0
- data/lib/bookbinder/deploy/distributor.rb +76 -0
- data/lib/bookbinder/deploy/failure.rb +15 -0
- data/lib/bookbinder/deploy/pusher.rb +34 -0
- data/lib/bookbinder/deploy/success.rb +16 -0
- data/lib/bookbinder/ingest/cloner_factory.rb +4 -4
- data/lib/bookbinder/ingest/local_filesystem_cloner.rb +5 -6
- data/lib/bookbinder/local_file_system_accessor.rb +9 -0
- data/lib/bookbinder/{post_production → postprocessing}/sitemap_writer.rb +7 -2
- data/lib/bookbinder/preprocessing/{copy_to_site_gen_dir.rb → link_to_site_gen_dir.rb} +2 -2
- data/lib/bookbinder/server_director.rb +3 -10
- data/lib/bookbinder/sheller.rb +5 -1
- data/master_middleman/bookbinder_helpers.rb +4 -12
- data/template_app/config.ru +3 -7
- data/template_app/rack_app.rb +23 -0
- metadata +60 -58
- data/lib/bookbinder/app_fetcher.rb +0 -36
- data/lib/bookbinder/archive.rb +0 -102
- data/lib/bookbinder/artifact_namer.rb +0 -22
- data/lib/bookbinder/commands/bookbinder_command.rb +0 -18
- data/lib/bookbinder/distributor.rb +0 -80
- data/lib/bookbinder/pusher.rb +0 -34
- data/lib/bookbinder/time_fetcher.rb +0 -7
- data/lib/bookbinder/values/blue_green_app.rb +0 -27
- data/lib/bookbinder/values/cf_routes.rb +0 -30
@@ -1,20 +1,19 @@
|
|
1
|
-
require_relative '../
|
2
|
-
require_relative '
|
1
|
+
require_relative '../deploy/archive'
|
2
|
+
require_relative '../deploy/deployment'
|
3
|
+
require_relative '../deploy/distributor'
|
3
4
|
require_relative 'naming'
|
4
5
|
|
5
6
|
module Bookbinder
|
6
|
-
class PushToProdValidator
|
7
|
-
MissingRequiredKeyError = Class.new(RuntimeError)
|
8
|
-
end
|
9
|
-
|
10
7
|
module Commands
|
11
8
|
class PushToProd
|
12
9
|
include Commands::Naming
|
13
|
-
|
10
|
+
MissingRequiredKeyError = Class.new(RuntimeError)
|
14
11
|
|
15
|
-
def initialize(logger, configuration_fetcher)
|
12
|
+
def initialize(streams, logger, configuration_fetcher, app_dir)
|
13
|
+
@streams = streams
|
16
14
|
@logger = logger
|
17
15
|
@configuration_fetcher = configuration_fetcher
|
16
|
+
@app_dir = app_dir
|
18
17
|
end
|
19
18
|
|
20
19
|
def usage
|
@@ -22,49 +21,46 @@ module Bookbinder
|
|
22
21
|
"Push latest or <build_#> from your S3 bucket to the production host specified in credentials.yml"]
|
23
22
|
end
|
24
23
|
|
25
|
-
def run(
|
24
|
+
def run((build_number))
|
25
|
+
streams[:warn].puts "Warning: You are pushing to production."
|
26
26
|
validate
|
27
|
-
|
27
|
+
deployment = Deploy::Deployment.new(
|
28
|
+
app_dir: app_dir,
|
29
|
+
build_number: build_number,
|
30
|
+
aws_credentials: credentials[:aws],
|
31
|
+
cf_credentials: credentials[:cloud_foundry],
|
32
|
+
book_repo: config.book_repo,
|
33
|
+
)
|
34
|
+
archive = Deploy::Archive.new(
|
35
|
+
logger: @logger,
|
36
|
+
key: deployment.aws_access_key,
|
37
|
+
secret: deployment.aws_secret_key
|
38
|
+
)
|
39
|
+
archive.download(download_dir: deployment.app_dir,
|
40
|
+
bucket: deployment.green_builds_bucket,
|
41
|
+
build_number: deployment.build_number,
|
42
|
+
namespace: deployment.namespace)
|
43
|
+
Deploy::Distributor.build(streams, archive, deployment).distribute
|
28
44
|
0
|
29
45
|
end
|
30
46
|
|
31
47
|
private
|
32
48
|
|
33
|
-
attr_reader :configuration_fetcher
|
34
|
-
|
35
|
-
def options(arguments)
|
36
|
-
{
|
37
|
-
app_dir: Dir.mktmpdir,
|
38
|
-
build_number: arguments[0],
|
39
|
-
|
40
|
-
aws_credentials: credentials[:aws],
|
41
|
-
cf_credentials: credentials[:cloud_foundry],
|
42
|
-
|
43
|
-
book_repo: config.book_repo,
|
44
|
-
}
|
45
|
-
end
|
49
|
+
attr_reader :app_dir, :configuration_fetcher, :streams
|
46
50
|
|
47
51
|
def credentials
|
48
52
|
configuration_fetcher.fetch_credentials('production')
|
49
53
|
end
|
50
54
|
|
51
55
|
def config
|
52
|
-
|
56
|
+
configuration_fetcher.fetch_config
|
53
57
|
end
|
54
58
|
|
55
59
|
def validate
|
56
|
-
|
57
|
-
|
58
|
-
unless config.has_option?(required_key)
|
59
|
-
missing_keys.push(required_key)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
if missing_keys.length > 0
|
64
|
-
raise PushToProdValidator::MissingRequiredKeyError.new "Your config.yml is missing required key(s). The require keys for this commands are " + missing_keys.join(", ")
|
60
|
+
unless config.has_option?('cred_repo')
|
61
|
+
raise MissingRequiredKeyError.new "Your config.yml is missing required key(s). The require keys for this commands are cred_repo"
|
65
62
|
end
|
66
63
|
end
|
67
|
-
|
68
64
|
end
|
69
65
|
end
|
70
66
|
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require_relative '../deprecated_logger'
|
2
1
|
require_relative '../errors/cli_error'
|
3
2
|
require_relative 'naming'
|
4
3
|
|
@@ -7,8 +6,8 @@ module Bookbinder
|
|
7
6
|
class Tag
|
8
7
|
include Commands::Naming
|
9
8
|
|
10
|
-
def initialize(
|
11
|
-
@
|
9
|
+
def initialize(streams, configuration_fetcher, version_control_system)
|
10
|
+
@streams = streams
|
12
11
|
@configuration_fetcher = configuration_fetcher
|
13
12
|
@version_control_system = version_control_system
|
14
13
|
end
|
@@ -24,14 +23,14 @@ module Bookbinder
|
|
24
23
|
version_control_system.remote_tag(url, tag, 'HEAD')
|
25
24
|
end
|
26
25
|
|
27
|
-
|
28
|
-
|
26
|
+
streams[:success].puts 'Success!'
|
27
|
+
streams[:out].puts "#{config.book_repo} and its sections were tagged with #{tag}"
|
29
28
|
0
|
30
29
|
end
|
31
30
|
|
32
31
|
private
|
33
32
|
|
34
|
-
attr_reader :configuration_fetcher, :version_control_system
|
33
|
+
attr_reader :streams, :configuration_fetcher, :version_control_system
|
35
34
|
|
36
35
|
def urls(config)
|
37
36
|
[config.book_repo_url] + config.sections.map(&:repo_url).uniq
|
@@ -22,25 +22,12 @@ module Bookbinder
|
|
22
22
|
]
|
23
23
|
end
|
24
24
|
|
25
|
-
def download_archive_before_push?
|
26
|
-
production?
|
27
|
-
end
|
28
|
-
|
29
|
-
def push_warning
|
30
|
-
if production?
|
31
|
-
'Warning: You are pushing to CF Docs production. Be careful.'
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
25
|
def routes
|
36
26
|
fetch(host_key) if correctly_formatted_domain_and_routes?(host_key)
|
37
27
|
end
|
38
28
|
|
39
29
|
def flat_routes
|
40
|
-
routes.
|
41
|
-
domain, apps = domain_apps
|
42
|
-
all_routes + apps.map { |app| [domain, app] }
|
43
|
-
end
|
30
|
+
routes.flat_map { |(domain, apps)| apps.map { |app| [domain, app] } }
|
44
31
|
end
|
45
32
|
|
46
33
|
def space
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'blue_green_app'
|
2
|
+
require_relative 'cf_routes'
|
3
|
+
|
4
|
+
module Bookbinder
|
5
|
+
module Deploy
|
6
|
+
class AppFetcher
|
7
|
+
def initialize(routes_to_search, cf_command_runner)
|
8
|
+
@routes_to_search = routes_to_search
|
9
|
+
@cf_command_runner = cf_command_runner
|
10
|
+
end
|
11
|
+
|
12
|
+
def fetch_current_app
|
13
|
+
raw_cf_routes = cf_command_runner.cf_routes_output
|
14
|
+
cf_routes = CfRoutes.new(raw_cf_routes)
|
15
|
+
|
16
|
+
existing_hosts = routes_to_search.select do |domain, host|
|
17
|
+
cf_routes.apps_by_host_and_domain.has_key?([host, domain])
|
18
|
+
end
|
19
|
+
|
20
|
+
return nil if existing_hosts.empty?
|
21
|
+
app_groups = existing_hosts.map { |domain, host| apps_for_host(cf_routes, domain, host) }
|
22
|
+
apps_for_existing_routes = app_groups.first
|
23
|
+
apps_for_existing_routes.first
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :routes_to_search, :cf_command_runner
|
29
|
+
|
30
|
+
def apps_for_host(routes_from_cf, domain, host)
|
31
|
+
routes_from_cf.apps_by_host_and_domain.fetch([host, domain], []).
|
32
|
+
map &BlueGreenApp.method(:new)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'fog/aws'
|
2
|
+
require 'tmpdir'
|
3
|
+
require_relative '../deprecated_logger'
|
4
|
+
require_relative 'artifact'
|
5
|
+
require_relative 'success'
|
6
|
+
|
7
|
+
module Bookbinder
|
8
|
+
module Deploy
|
9
|
+
class Archive
|
10
|
+
class FileDoesNotExist < StandardError; end
|
11
|
+
class NoNamespaceGiven < StandardError; end
|
12
|
+
|
13
|
+
def initialize(logger: nil, key: '', secret: '')
|
14
|
+
@logger = logger
|
15
|
+
@aws_key = key
|
16
|
+
@aws_secret_key = secret
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_and_upload_tarball(
|
20
|
+
app_dir: 'final_app',
|
21
|
+
bucket: '',
|
22
|
+
build_number: nil,
|
23
|
+
namespace: nil
|
24
|
+
)
|
25
|
+
tarball_filename, tarball_path = create_tarball(app_dir, build_number, namespace)
|
26
|
+
upload_file(bucket, tarball_filename, tarball_path)
|
27
|
+
Success.new("Green build ##{build_number} has been uploaded to S3 for #{namespace}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def upload_file(bucket, name, source_path)
|
31
|
+
find_or_create_directory(bucket).
|
32
|
+
files.create(key: name,
|
33
|
+
body: File.read(source_path),
|
34
|
+
public: true)
|
35
|
+
end
|
36
|
+
|
37
|
+
def download(download_dir: nil, bucket: nil, build_number: nil, namespace: nil)
|
38
|
+
raise NoNamespaceGiven, 'One must specify a namespace to find files in this bucket' unless namespace
|
39
|
+
|
40
|
+
directory = connection.directories.get bucket
|
41
|
+
build_number ||= latest_build_number_for_namespace(directory, namespace)
|
42
|
+
filename = Artifact.new(namespace, build_number, 'tgz').filename
|
43
|
+
|
44
|
+
s3_file = directory.files.get(filename)
|
45
|
+
raise FileDoesNotExist, "Unable to find tarball on AWS for book '#{namespace}', build number: #{build_number}" unless s3_file
|
46
|
+
|
47
|
+
downloaded_file = File.join(Dir.mktmpdir, 'downloaded.tgz')
|
48
|
+
File.open(downloaded_file, 'wb') { |f| f.write(s3_file.body) }
|
49
|
+
Dir.chdir(download_dir) { `tar xzf #{downloaded_file}` }
|
50
|
+
|
51
|
+
@logger.log "Green build ##{build_number.to_s.green} has been downloaded from S3 and untarred into #{download_dir.to_s.cyan}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def tarball_name_regex(namespace)
|
55
|
+
/^#{namespace}-(\d+_?\d*)\.tgz/
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def find_or_create_directory(name)
|
61
|
+
connection.directories.create(key: name)
|
62
|
+
rescue Excon::Errors::Conflict
|
63
|
+
connection.directories.get(name)
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_tarball(app_dir, build_number, namespace)
|
67
|
+
tarball_filename = Artifact.new(namespace, build_number, 'tgz').filename
|
68
|
+
tarball_path = File.join(Dir.mktmpdir, tarball_filename)
|
69
|
+
|
70
|
+
Dir.chdir(app_dir) { `tar czf #{tarball_path} *` }
|
71
|
+
return tarball_filename, tarball_path
|
72
|
+
end
|
73
|
+
|
74
|
+
def latest_build_number_for_namespace(directory, namespace)
|
75
|
+
all_files = all_files_workaround_for_fog_map_limitation(directory.files)
|
76
|
+
|
77
|
+
all_files_with_namespace = all_files.map do |file|
|
78
|
+
filename = file.key
|
79
|
+
file if filename[tarball_name_regex(namespace)]
|
80
|
+
end.compact
|
81
|
+
|
82
|
+
return nil if all_files_with_namespace.empty?
|
83
|
+
most_recent_file = all_files_with_namespace.sort_by { |file| file.last_modified }.last
|
84
|
+
|
85
|
+
most_recent_filename = most_recent_file.key
|
86
|
+
most_recent_filename[tarball_name_regex(namespace), 1]
|
87
|
+
end
|
88
|
+
|
89
|
+
def connection
|
90
|
+
@connection ||= Fog::Storage.new :provider => 'AWS',
|
91
|
+
:aws_access_key_id => @aws_key,
|
92
|
+
:aws_secret_access_key => @aws_secret_key
|
93
|
+
end
|
94
|
+
|
95
|
+
def all_files_workaround_for_fog_map_limitation(files)
|
96
|
+
all_files = []
|
97
|
+
files.each { |f| all_files << f }
|
98
|
+
all_files
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Bookbinder
|
2
|
+
module Deploy
|
3
|
+
class Artifact
|
4
|
+
attr_reader :namespace
|
5
|
+
|
6
|
+
def initialize(namespace, build_number, extension, path = '.')
|
7
|
+
@namespace = namespace
|
8
|
+
@build_number = build_number
|
9
|
+
@path = path
|
10
|
+
@extension = extension
|
11
|
+
end
|
12
|
+
|
13
|
+
def full_path
|
14
|
+
File.join(path, filename)
|
15
|
+
end
|
16
|
+
|
17
|
+
def filename
|
18
|
+
"#{namespace}-#{build_number}.#{extension}"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :build_number, :path, :extension
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Bookbinder
|
2
|
+
module Deploy
|
3
|
+
class BlueGreenApp
|
4
|
+
def initialize(name)
|
5
|
+
@name = name.strip
|
6
|
+
end
|
7
|
+
|
8
|
+
def ==(other)
|
9
|
+
to_s == other.to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
name
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_flipped_name
|
17
|
+
if name.match(/green$/)
|
18
|
+
BlueGreenApp.new(name.sub(/green$/, 'blue'))
|
19
|
+
else
|
20
|
+
BlueGreenApp.new(name.sub(/blue$/, 'green'))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Bookbinder
|
2
|
+
module Deploy
|
3
|
+
class CfRoutes
|
4
|
+
def initialize(raw_routes)
|
5
|
+
@raw_routes = raw_routes
|
6
|
+
end
|
7
|
+
|
8
|
+
def apps_by_host_and_domain
|
9
|
+
@apps_by_host_and_domain ||= data(raw_routes).reduce({}) {|acc, row|
|
10
|
+
parsed_row = Hash[headers(raw_routes).zip(row)]
|
11
|
+
acc.merge(parsed_row.values_at('host', 'domain') => parse_apps(parsed_row['apps']))
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :raw_routes
|
18
|
+
|
19
|
+
def parse_apps(apps)
|
20
|
+
apps.split(',').map(&:strip)
|
21
|
+
end
|
22
|
+
|
23
|
+
def headers(raw)
|
24
|
+
raw.lines[2].split(/\s+/)
|
25
|
+
end
|
26
|
+
|
27
|
+
def data(raw)
|
28
|
+
raw.lines[3..-1].map {|line| line.split(/\s+/, headers(raw).size)}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Bookbinder
|
2
|
+
module Deploy
|
3
|
+
class Deployment
|
4
|
+
attr_reader :app_dir, :build_number, :cf_credentials
|
5
|
+
|
6
|
+
def initialize(app_dir: nil,
|
7
|
+
aws_credentials: nil,
|
8
|
+
book_repo: nil,
|
9
|
+
build_number: nil,
|
10
|
+
cf_credentials: nil)
|
11
|
+
@app_dir = app_dir
|
12
|
+
@aws_credentials = aws_credentials
|
13
|
+
@book_repo = book_repo
|
14
|
+
@build_number = build_number
|
15
|
+
@cf_credentials = cf_credentials
|
16
|
+
end
|
17
|
+
|
18
|
+
def artifact_filename
|
19
|
+
artifact.filename
|
20
|
+
end
|
21
|
+
|
22
|
+
def artifact_full_path
|
23
|
+
artifact.full_path
|
24
|
+
end
|
25
|
+
|
26
|
+
def aws_access_key
|
27
|
+
aws_credentials.access_key
|
28
|
+
end
|
29
|
+
|
30
|
+
def aws_secret_key
|
31
|
+
aws_credentials.secret_key
|
32
|
+
end
|
33
|
+
|
34
|
+
def flat_routes
|
35
|
+
cf_credentials.flat_routes
|
36
|
+
end
|
37
|
+
|
38
|
+
def green_builds_bucket
|
39
|
+
aws_credentials.green_builds_bucket
|
40
|
+
end
|
41
|
+
|
42
|
+
def namespace
|
43
|
+
Ingest::DestinationDirectory.new(book_repo)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :aws_credentials, :book_repo
|
49
|
+
|
50
|
+
def artifact
|
51
|
+
Artifact.new(namespace, build_number, 'log', '/tmp')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|