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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bookbinder/cf_command_runner.rb +33 -30
  3. data/lib/bookbinder/cli.rb +15 -7
  4. data/lib/bookbinder/code_example_reader.rb +5 -7
  5. data/lib/bookbinder/commands/bind/directory_preparer.rb +1 -1
  6. data/lib/bookbinder/commands/build_and_push_tarball.rb +18 -6
  7. data/lib/bookbinder/commands/collection.rb +16 -21
  8. data/lib/bookbinder/commands/push_from_local.rb +27 -19
  9. data/lib/bookbinder/commands/push_to_prod.rb +30 -34
  10. data/lib/bookbinder/commands/tag.rb +5 -6
  11. data/lib/bookbinder/commands/update_local_doc_repos.rb +0 -1
  12. data/lib/bookbinder/config/cf_credentials.rb +1 -14
  13. data/lib/bookbinder/deploy/app_fetcher.rb +36 -0
  14. data/lib/bookbinder/deploy/archive.rb +102 -0
  15. data/lib/bookbinder/deploy/artifact.rb +26 -0
  16. data/lib/bookbinder/deploy/blue_green_app.rb +29 -0
  17. data/lib/bookbinder/deploy/cf_routes.rb +32 -0
  18. data/lib/bookbinder/deploy/deployment.rb +55 -0
  19. data/lib/bookbinder/deploy/distributor.rb +76 -0
  20. data/lib/bookbinder/deploy/failure.rb +15 -0
  21. data/lib/bookbinder/deploy/pusher.rb +34 -0
  22. data/lib/bookbinder/deploy/success.rb +16 -0
  23. data/lib/bookbinder/ingest/cloner_factory.rb +4 -4
  24. data/lib/bookbinder/ingest/local_filesystem_cloner.rb +5 -6
  25. data/lib/bookbinder/local_file_system_accessor.rb +9 -0
  26. data/lib/bookbinder/{post_production → postprocessing}/sitemap_writer.rb +7 -2
  27. data/lib/bookbinder/preprocessing/{copy_to_site_gen_dir.rb → link_to_site_gen_dir.rb} +2 -2
  28. data/lib/bookbinder/server_director.rb +3 -10
  29. data/lib/bookbinder/sheller.rb +5 -1
  30. data/master_middleman/bookbinder_helpers.rb +4 -12
  31. data/template_app/config.ru +3 -7
  32. data/template_app/rack_app.rb +23 -0
  33. metadata +60 -58
  34. data/lib/bookbinder/app_fetcher.rb +0 -36
  35. data/lib/bookbinder/archive.rb +0 -102
  36. data/lib/bookbinder/artifact_namer.rb +0 -22
  37. data/lib/bookbinder/commands/bookbinder_command.rb +0 -18
  38. data/lib/bookbinder/distributor.rb +0 -80
  39. data/lib/bookbinder/pusher.rb +0 -34
  40. data/lib/bookbinder/time_fetcher.rb +0 -7
  41. data/lib/bookbinder/values/blue_green_app.rb +0 -27
  42. data/lib/bookbinder/values/cf_routes.rb +0 -30
@@ -1,20 +1,19 @@
1
- require_relative '../distributor'
2
- require_relative 'bookbinder_command'
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
- CONFIG_REQUIRED_KEYS = %w(cred_repo)
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(arguments)
24
+ def run((build_number))
25
+ streams[:warn].puts "Warning: You are pushing to production."
26
26
  validate
27
- Distributor.build(@logger, options(arguments)).distribute
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
- @config ||= configuration_fetcher.fetch_config
56
+ configuration_fetcher.fetch_config
53
57
  end
54
58
 
55
59
  def validate
56
- missing_keys = []
57
- CONFIG_REQUIRED_KEYS.map do |required_key|
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(logger, configuration_fetcher, version_control_system)
11
- @logger = logger
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
- @logger.log 'Success!'.green
28
- @logger.log " #{config.book_repo.yellow} and its sections were tagged with #{tag.blue}"
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
@@ -1,4 +1,3 @@
1
- require_relative '../deprecated_logger'
2
1
  require_relative '../ingest/destination_directory'
3
2
  require_relative 'naming'
4
3
 
@@ -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.reduce([]) do |all_routes, domain_apps|
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