bookbindery 1.0.0

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