bookbindery 1.0.0

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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bookbinder +6 -0
  3. data/lib/bookbinder.rb +59 -0
  4. data/lib/bookbinder/app_fetcher.rb +40 -0
  5. data/lib/bookbinder/archive.rb +95 -0
  6. data/lib/bookbinder/artifact_namer.rb +22 -0
  7. data/lib/bookbinder/blue_green_app.rb +27 -0
  8. data/lib/bookbinder/book.rb +54 -0
  9. data/lib/bookbinder/bookbinder_logger.rb +33 -0
  10. data/lib/bookbinder/cf_command_runner.rb +114 -0
  11. data/lib/bookbinder/cf_routes.rb +19 -0
  12. data/lib/bookbinder/cli.rb +68 -0
  13. data/lib/bookbinder/cli_error.rb +6 -0
  14. data/lib/bookbinder/code_example.rb +40 -0
  15. data/lib/bookbinder/command_runner.rb +32 -0
  16. data/lib/bookbinder/command_validator.rb +24 -0
  17. data/lib/bookbinder/commands/bookbinder_command.rb +18 -0
  18. data/lib/bookbinder/commands/build_and_push_tarball.rb +31 -0
  19. data/lib/bookbinder/commands/generate_pdf.rb +140 -0
  20. data/lib/bookbinder/commands/help.rb +31 -0
  21. data/lib/bookbinder/commands/naming.rb +9 -0
  22. data/lib/bookbinder/commands/publish.rb +138 -0
  23. data/lib/bookbinder/commands/push_local_to_staging.rb +35 -0
  24. data/lib/bookbinder/commands/push_to_prod.rb +35 -0
  25. data/lib/bookbinder/commands/run_publish_ci.rb +42 -0
  26. data/lib/bookbinder/commands/tag.rb +31 -0
  27. data/lib/bookbinder/commands/update_local_doc_repos.rb +27 -0
  28. data/lib/bookbinder/commands/version.rb +25 -0
  29. data/lib/bookbinder/configuration.rb +163 -0
  30. data/lib/bookbinder/configuration_fetcher.rb +55 -0
  31. data/lib/bookbinder/configuration_validator.rb +162 -0
  32. data/lib/bookbinder/css_link_checker.rb +64 -0
  33. data/lib/bookbinder/directory_helpers.rb +15 -0
  34. data/lib/bookbinder/distributor.rb +69 -0
  35. data/lib/bookbinder/git_client.rb +63 -0
  36. data/lib/bookbinder/git_hub_repository.rb +151 -0
  37. data/lib/bookbinder/local_file_system_accessor.rb +9 -0
  38. data/lib/bookbinder/middleman_runner.rb +86 -0
  39. data/lib/bookbinder/pdf_generator.rb +73 -0
  40. data/lib/bookbinder/publisher.rb +125 -0
  41. data/lib/bookbinder/pusher.rb +34 -0
  42. data/lib/bookbinder/remote_yaml_credential_provider.rb +21 -0
  43. data/lib/bookbinder/section.rb +78 -0
  44. data/lib/bookbinder/server_director.rb +53 -0
  45. data/lib/bookbinder/shell_out.rb +19 -0
  46. data/lib/bookbinder/sieve.rb +62 -0
  47. data/lib/bookbinder/sitemap_generator.rb +19 -0
  48. data/lib/bookbinder/spider.rb +91 -0
  49. data/lib/bookbinder/stabilimentum.rb +59 -0
  50. data/lib/bookbinder/usage_messenger.rb +33 -0
  51. data/lib/bookbinder/yaml_loader.rb +22 -0
  52. data/master_middleman/bookbinder_helpers.rb +133 -0
  53. data/master_middleman/config.rb +23 -0
  54. data/master_middleman/quicklinks_renderer.rb +78 -0
  55. data/master_middleman/submodule_aware_assets.rb +45 -0
  56. data/template_app/Gemfile +7 -0
  57. data/template_app/Gemfile.lock +20 -0
  58. data/template_app/app.rb +3 -0
  59. data/template_app/config.ru +9 -0
  60. data/template_app/lib/rack_static.rb +19 -0
  61. data/template_app/lib/vienna_application.rb +26 -0
  62. metadata +462 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 19c1ec695ede30996441a72bccd9617f0802d6d1
4
+ data.tar.gz: e711b4cfe5f5af59d1e4ec68d0c98975295eab60
5
+ SHA512:
6
+ metadata.gz: 1ab45230d391a2aac4d84ff67015bd48d44e82fbfb55188bbf03db798efada981a635a6d7d15e71e763eff98b45e1798850f85f0e41f1ffc346785782f683abb
7
+ data.tar.gz: 04f5b2b527d2c90a99b47942e6b440054d47e4e6ac74712cdb4c4fd8b80d0efb67a5a263fd7ea6cc615e6262554f0cb3824f9783259eeee5fc4025809a26a78b
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bookbinder'
4
+
5
+ return_code = Bookbinder::Cli.new.run ARGV
6
+ exit return_code.to_i
@@ -0,0 +1,59 @@
1
+ require 'fog'
2
+ require 'tmpdir'
3
+ require 'ansi'
4
+ require 'faraday'
5
+ require 'faraday_middleware'
6
+ require 'octokit'
7
+ require 'padrino-contrib'
8
+ require 'middleman-syntax'
9
+ require 'middleman-core/cli'
10
+ require 'middleman-core/profiling'
11
+ require 'anemone'
12
+ require 'css_parser'
13
+ require 'vienna'
14
+ require 'popen4'
15
+ require 'puma'
16
+
17
+ #require_relative 'bookbinder/shell_out'
18
+ require_relative 'bookbinder/bookbinder_logger'
19
+ require_relative 'bookbinder/git_client'
20
+ #require_relative 'bookbinder/repository'
21
+ require_relative 'bookbinder/section'
22
+ require_relative 'bookbinder/book'
23
+ require_relative 'bookbinder/code_example'
24
+ require_relative 'bookbinder/remote_yaml_credential_provider'
25
+ require_relative 'bookbinder/configuration'
26
+ require_relative 'bookbinder/configuration_fetcher'
27
+ require_relative 'bookbinder/configuration_validator'
28
+ require_relative 'bookbinder/css_link_checker'
29
+ require_relative 'bookbinder/sitemap_generator'
30
+ require_relative 'bookbinder/sieve'
31
+ require_relative 'bookbinder/stabilimentum'
32
+ require_relative 'bookbinder/server_director'
33
+ require_relative 'bookbinder/spider'
34
+
35
+ require_relative 'bookbinder/archive'
36
+ require_relative 'bookbinder/pdf_generator'
37
+ require_relative 'bookbinder/middleman_runner'
38
+ require_relative 'bookbinder/publisher'
39
+ require_relative 'bookbinder/cf_command_runner'
40
+ require_relative 'bookbinder/pusher'
41
+ require_relative 'bookbinder/artifact_namer'
42
+ require_relative 'bookbinder/distributor'
43
+
44
+ require_relative 'bookbinder/commands/bookbinder_command'
45
+ require_relative 'bookbinder/commands/build_and_push_tarball'
46
+ require_relative 'bookbinder/commands/publish'
47
+ require_relative 'bookbinder/commands/push_local_to_staging'
48
+ require_relative 'bookbinder/commands/push_to_prod'
49
+ require_relative 'bookbinder/commands/run_publish_ci'
50
+ require_relative 'bookbinder/commands/update_local_doc_repos'
51
+ require_relative 'bookbinder/commands/tag'
52
+ require_relative 'bookbinder/commands/generate_pdf'
53
+
54
+ require_relative 'bookbinder/usage_messenger'
55
+
56
+ require_relative 'bookbinder/cli'
57
+
58
+ # Finds the project root for both spec & production
59
+ GEM_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
@@ -0,0 +1,40 @@
1
+ require_relative "blue_green_app"
2
+ require_relative "cf_routes"
3
+
4
+ module Bookbinder
5
+
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
+ app_groups =
21
+ if existing_hosts.any?
22
+ existing_hosts.map { |domain, host| apps_for_host(cf_routes, domain, host) }
23
+ else
24
+ raise "cannot find currently deployed app."
25
+ end
26
+ apps_for_existing_routes = app_groups.first
27
+ apps_for_existing_routes.first
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :routes_to_search, :cf_command_runner
33
+
34
+ def apps_for_host(routes_from_cf, domain, host)
35
+ routes_from_cf.apps_by_host_and_domain.fetch([host, domain], []).
36
+ map &BlueGreenApp.method(:new)
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,95 @@
1
+ require 'fog'
2
+ require 'tmpdir'
3
+ require_relative 'bookbinder_logger'
4
+ require_relative 'artifact_namer'
5
+
6
+ module Bookbinder
7
+ class Archive
8
+ class FileDoesNotExist < StandardError; end
9
+ class NoNamespaceGiven < StandardError; end
10
+
11
+ def initialize(logger: nil, key: '', secret: '')
12
+ @logger = logger
13
+ @aws_key = key
14
+ @aws_secret_key = secret
15
+ end
16
+
17
+ def create_and_upload_tarball(build_number: nil, app_dir: 'final_app', bucket: '', namespace: nil)
18
+ raise 'You must provide a build_number to push an identifiable build.' unless build_number
19
+ raise 'You must provide a namespace to push an identifiable build.' unless namespace
20
+
21
+ tarball_filename, tarball_path = create_tarball(app_dir, build_number, namespace)
22
+
23
+ upload_file(bucket, tarball_filename, tarball_path)
24
+ @logger.log "Green build ##{build_number.to_s.green} has been uploaded to S3 for #{namespace.to_s.cyan}"
25
+ end
26
+
27
+ def upload_file(bucket, name, source_path)
28
+ find_or_create_directory(bucket).
29
+ files.create(key: name,
30
+ body: File.read(source_path),
31
+ public: true)
32
+ end
33
+
34
+ def download(download_dir: nil, bucket: nil, build_number: nil, namespace: nil)
35
+ raise NoNamespaceGiven, 'One must specify a namespace to find files in this bucket' unless namespace
36
+
37
+ directory = connection.directories.get bucket
38
+ build_number ||= latest_build_number_for_namespace(directory, namespace)
39
+ filename = ArtifactNamer.new(namespace, build_number, 'tgz').filename
40
+
41
+ s3_file = directory.files.get(filename)
42
+ raise FileDoesNotExist, "Unable to find tarball on AWS for book '#{namespace}', build number: #{build_number}" unless s3_file
43
+
44
+ downloaded_file = File.join(Dir.mktmpdir, 'downloaded.tgz')
45
+ File.open(downloaded_file, 'wb') { |f| f.write(s3_file.body) }
46
+ Dir.chdir(download_dir) { `tar xzf #{downloaded_file}` }
47
+
48
+ @logger.log "Green build ##{build_number.to_s.green} has been downloaded from S3 and untarred into #{download_dir.to_s.cyan}"
49
+ end
50
+
51
+ private
52
+
53
+ def find_or_create_directory(name)
54
+ connection.directories.create(key: name)
55
+ rescue Excon::Errors::Conflict
56
+ connection.directories.get(name)
57
+ end
58
+
59
+ def create_tarball(app_dir, build_number, namespace)
60
+ tarball_filename = ArtifactNamer.new(namespace, build_number, 'tgz').filename
61
+ tarball_path = File.join(Dir.mktmpdir, tarball_filename)
62
+
63
+ Dir.chdir(app_dir) { `tar czf #{tarball_path} *` }
64
+ return tarball_filename, tarball_path
65
+ end
66
+
67
+ def latest_build_number_for_namespace(directory, namespace)
68
+ all_files = all_files_workaround_for_fog_map_limitation(directory.files)
69
+
70
+ all_files_with_namespace = all_files.map do |file|
71
+ filename = file.key
72
+ matches = /^#{namespace}-([\d]+)\.tgz/.match(filename)
73
+ file if matches
74
+ end.compact
75
+
76
+ return nil if all_files_with_namespace.empty?
77
+ most_recent_file = all_files_with_namespace.sort_by { |file| file.last_modified }.last
78
+
79
+ most_recent_filename = most_recent_file.key
80
+ most_recent_build_number = /^#{namespace}-([\d]+)\.tgz/.match(most_recent_filename)[1]
81
+ end
82
+
83
+ def connection
84
+ @connection ||= Fog::Storage.new :provider => 'AWS',
85
+ :aws_access_key_id => @aws_key,
86
+ :aws_secret_access_key => @aws_secret_key
87
+ end
88
+
89
+ def all_files_workaround_for_fog_map_limitation(files)
90
+ all_files = []
91
+ files.each { |f| all_files << f }
92
+ all_files
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,22 @@
1
+ module Bookbinder
2
+ class ArtifactNamer
3
+ def initialize(namespace, build_number, extension, path = '.')
4
+ @namespace = namespace
5
+ @build_number = build_number
6
+ @path = path
7
+ @extension = extension
8
+ end
9
+
10
+ def full_path
11
+ File.join(path, filename)
12
+ end
13
+
14
+ def filename
15
+ "#{namespace}-#{build_number}.#{extension}"
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :namespace, :build_number, :path, :extension
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ module Bookbinder
2
+ class BlueGreenApp
3
+ def initialize(name)
4
+ @name = name.strip
5
+ end
6
+
7
+ def ==(other)
8
+ to_s == other.to_s
9
+ end
10
+
11
+ def to_s
12
+ name
13
+ end
14
+
15
+ def with_flipped_name
16
+ if name.match(/green$/)
17
+ BlueGreenApp.new(name.sub(/green$/, 'blue'))
18
+ else
19
+ BlueGreenApp.new(name.sub(/blue$/, 'green'))
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :name
26
+ end
27
+ end
@@ -0,0 +1,54 @@
1
+ require_relative 'git_hub_repository'
2
+ require_relative 'directory_helpers'
3
+
4
+ module Bookbinder
5
+ class Book
6
+ include DirectoryHelperMethods
7
+ attr_reader :sections
8
+
9
+ def self.from_remote(logger: nil, full_name: nil, destination_dir: nil, ref: nil, git_accessor: Git)
10
+ book = new(logger: logger, full_name: full_name, target_ref: ref, git_accessor: git_accessor)
11
+ book.copy_from_remote(destination_dir) if destination_dir
12
+ book
13
+ end
14
+
15
+ def initialize(logger: nil, full_name: nil, target_ref: nil, github_token: nil, sections: [], git_accessor: Git)
16
+ @sections = sections.map do |section|
17
+ GitHubRepository.new logger: logger, full_name: section['repository']['name']
18
+ end
19
+
20
+ @repository = GitHubRepository.new(logger: logger, full_name: full_name, target_ref: target_ref, github_token: github_token)
21
+ @git_accessor = git_accessor
22
+ end
23
+
24
+ def full_name
25
+ @repository.full_name
26
+ end
27
+
28
+ def head_sha
29
+ @repository.head_sha
30
+ end
31
+
32
+ def directory
33
+ @repository.directory
34
+ end
35
+
36
+ def get_modification_date_for(file: nil, full_path: nil)
37
+ git_directory, file_relative_path = full_path.split(output_dir_name+'/')
38
+ begin
39
+ git_base_object = Git.open(git_directory)
40
+ rescue => e
41
+ raise "Invalid git repository! #{git_directory} is not a .git directory"
42
+ end
43
+ @repository.get_modification_date_for(file: file_relative_path, git: git_base_object)
44
+ end
45
+
46
+ def copy_from_remote(destination_dir)
47
+ @repository.copy_from_remote(destination_dir, @git_accessor)
48
+ end
49
+
50
+ def tag_self_and_sections_with(tag)
51
+ (@sections + [@repository]).each { |repo| repo.tag_with tag }
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,33 @@
1
+ require 'ansi'
2
+
3
+ class String
4
+ include ANSI::Mixin
5
+ end
6
+
7
+ module Bookbinder
8
+ class BookbinderLogger
9
+ def log(message)
10
+ puts message
11
+ end
12
+
13
+ def log_print(message)
14
+ print message
15
+ end
16
+
17
+ def error(message)
18
+ puts message.red
19
+ end
20
+
21
+ def success(message)
22
+ puts message.green
23
+ end
24
+
25
+ def warn(message)
26
+ puts message.yellow
27
+ end
28
+
29
+ def notify(message)
30
+ puts message.blue
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,114 @@
1
+ require 'open3'
2
+ require_relative 'blue_green_app'
3
+
4
+ module Bookbinder
5
+ class CfCommandRunner
6
+ def initialize(logger, cf_credentials, trace_file)
7
+ @logger = logger
8
+ @creds = cf_credentials
9
+ @trace_file = trace_file
10
+ end
11
+
12
+ def login
13
+ username = creds.username
14
+ password = creds.password
15
+ api_endpoint = creds.api_endpoint
16
+ organization = creds.organization
17
+ space = creds.space
18
+ creds_string = (username && password) ? "-u '#{username}' -p '#{password}'" : ''
19
+
20
+ success = Kernel.system("#{cf_binary_path} login #{creds_string} -a '#{api_endpoint}' -o '#{organization}' -s '#{space}'")
21
+ raise "Could not log in to #{creds.api_endpoint}" unless success
22
+ end
23
+
24
+ def cf_routes_output
25
+ output, status = Open3.capture2("CF_COLOR=false #{cf_binary_path} routes")
26
+ raise 'failure executing cf routes' unless status.success?
27
+ output
28
+ end
29
+
30
+ def new_app
31
+ BlueGreenApp.new([creds.app_name, 'blue'].join('-'))
32
+ end
33
+
34
+ def start(deploy_target_app)
35
+ # Theoretically we shouldn't need this (and corresponding "stop" below), but we've seen CF pull files from both
36
+ # green and blue when a DNS redirect points to HOST.cfapps.io
37
+ # Also, shutting down the unused app saves $$
38
+ Kernel.system("#{cf_binary_path} start #{deploy_target_app} ")
39
+ end
40
+
41
+ def push(deploy_target_app)
42
+ # Currently --no-routes is used to blow away all existing routes from a newly deployed app.
43
+ # The routes will then be recreated from the creds repo.
44
+ success = Kernel.system(environment_variables, "#{cf_binary_path} push #{deploy_target_app} --no-route -m 256M -i 3")
45
+ raise "Could not deploy app to #{deploy_target_app}" unless success
46
+ end
47
+
48
+ def unmap_routes(app)
49
+ creds.flat_routes.each do |domain, host|
50
+ unmap_route(app, domain, host)
51
+ end
52
+ end
53
+
54
+ def map_routes(app)
55
+ succeeded = []
56
+
57
+ creds.flat_routes.each do |domain, name|
58
+ begin
59
+ map_route(app, domain, name)
60
+ succeeded << [app, domain, name]
61
+ rescue RuntimeError
62
+ succeeded.each { |app, domain, host| unmap_route(app, domain, host) }
63
+ raise
64
+ end
65
+ end
66
+ end
67
+
68
+ def takedown_old_target_app(app)
69
+ # Routers flush every 10 seconds (but not guaranteed), so wait a bit longer than that.
70
+ @logger.log "waiting 15 seconds for routes to remap...\n\n"
71
+ (1..15).to_a.reverse.each do |seconds|
72
+ @logger.log_print "\r\r#{seconds}... "
73
+ Kernel.sleep 1
74
+ end
75
+ stop(app)
76
+ unmap_routes(app)
77
+ end
78
+
79
+ private
80
+
81
+ attr_reader :creds
82
+
83
+ def stop(app)
84
+ success = Kernel.system("#{cf_binary_path} stop #{app}")
85
+ raise "Failed to stop application #{app}" unless success
86
+ end
87
+
88
+ def map_route(deploy_target_app, domain, host)
89
+ map_route_command = "#{cf_binary_path} map-route #{deploy_target_app} #{domain}"
90
+ map_route_command += " -n #{host}" unless host.empty?
91
+
92
+ success = Kernel.system(map_route_command)
93
+ raise "Deployed app to #{deploy_target_app} but failed to map hostname #{host}.#{domain} to it." unless success
94
+ end
95
+
96
+ def unmap_route(deploy_target_app, domain, host)
97
+ unmap_route_command = "#{cf_binary_path} unmap-route #{deploy_target_app} #{domain}"
98
+ unmap_route_command += " -n #{host}" unless host.empty?
99
+
100
+ success = Kernel.system(unmap_route_command)
101
+ raise "Failed to unmap route #{host} on #{deploy_target_app}." unless success
102
+ end
103
+
104
+ def cf_binary_path
105
+ @cf_binary_path ||= `which cf`.chomp!
106
+ raise "CF CLI could not be found in your PATH. Please make sure cf cli is in your PATH." if @cf_binary_path.nil?
107
+ @cf_binary_path
108
+ end
109
+
110
+ def environment_variables
111
+ {'CF_TRACE' => @trace_file}
112
+ end
113
+ end
114
+ end