bookbindery 4.1.0 → 4.1.1

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