buildpack-packager 2.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +5 -0
  5. data/LICENSE +176 -0
  6. data/README.md +180 -0
  7. data/Rakefile +6 -0
  8. data/bin/buildpack-packager +79 -0
  9. data/buildpack-packager.gemspec +33 -0
  10. data/doc/disconnected_environments.md +50 -0
  11. data/lib/buildpack/manifest_dependency.rb +14 -0
  12. data/lib/buildpack/manifest_validator.rb +88 -0
  13. data/lib/buildpack/packager.rb +55 -0
  14. data/lib/buildpack/packager/default_versions_presenter.rb +38 -0
  15. data/lib/buildpack/packager/dependencies_presenter.rb +41 -0
  16. data/lib/buildpack/packager/manifest_schema.yml +67 -0
  17. data/lib/buildpack/packager/package.rb +139 -0
  18. data/lib/buildpack/packager/table_presentation.rb +21 -0
  19. data/lib/buildpack/packager/version.rb +5 -0
  20. data/lib/buildpack/packager/zip_file_excluder.rb +31 -0
  21. data/lib/kwalify/parser/yaml-patcher.rb +70 -0
  22. data/spec/buildpack/packager_spec.rb +7 -0
  23. data/spec/fixtures/buildpack-with-uri-credentials/VERSION +1 -0
  24. data/spec/fixtures/buildpack-with-uri-credentials/manifest.yml +36 -0
  25. data/spec/fixtures/buildpack-without-uri-credentials/VERSION +1 -0
  26. data/spec/fixtures/buildpack-without-uri-credentials/manifest.yml +36 -0
  27. data/spec/fixtures/manifests/manifest_invalid-md6.yml +18 -0
  28. data/spec/fixtures/manifests/manifest_invalid-md6_and_defaults.yml +21 -0
  29. data/spec/fixtures/manifests/manifest_valid.yml +19 -0
  30. data/spec/helpers/cache_directory_helpers.rb +15 -0
  31. data/spec/helpers/fake_binary_hosting_helpers.rb +21 -0
  32. data/spec/helpers/file_system_helpers.rb +35 -0
  33. data/spec/integration/bin/buildpack_packager/download_caching_spec.rb +85 -0
  34. data/spec/integration/bin/buildpack_packager_spec.rb +378 -0
  35. data/spec/integration/buildpack/directory_name_spec.rb +89 -0
  36. data/spec/integration/buildpack/packager_spec.rb +454 -0
  37. data/spec/integration/default_versions_spec.rb +170 -0
  38. data/spec/integration/output_spec.rb +70 -0
  39. data/spec/spec_helper.rb +12 -0
  40. data/spec/unit/buildpack/packager/zip_file_excluder_spec.rb +68 -0
  41. data/spec/unit/manifest_dependency_spec.rb +41 -0
  42. data/spec/unit/manifest_validator_spec.rb +35 -0
  43. data/spec/unit/packager/package_spec.rb +196 -0
  44. metadata +235 -0
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'buildpack/packager/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'buildpack-packager'
8
+ spec.version = Buildpack::Packager::VERSION
9
+ spec.authors = ['Cloud Foundry Buildpacks Team']
10
+ spec.email = ['cf-buildpacks-eng@pivotal.io']
11
+ spec.summary = 'Tool that packages your buildpacks based on a manifest'
12
+ spec.description = 'Tool that packages your Cloud Foundry buildpacks based on a manifest'
13
+ spec.homepage = 'https://github.com/cloudfoundry/buildpack-packager'
14
+ spec.license = 'Apache-2.0'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.required_ruby_version = '~> 2.2'
22
+
23
+ spec.add_dependency 'activesupport', '~> 4.1'
24
+ spec.add_dependency 'kwalify', '~> 0'
25
+ spec.add_dependency 'terminal-table', '~> 1.4'
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.7'
28
+ spec.add_development_dependency 'rake', '~> 10.0'
29
+ spec.add_development_dependency 'rspec', '~> 3.5'
30
+ spec.add_development_dependency 'rubyzip', '~> 1.2'
31
+ spec.add_development_dependency 'rubocop', '~> 0'
32
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.5'
33
+ end
@@ -0,0 +1,50 @@
1
+ # Disconnected environments
2
+
3
+ ### Deploying Apps on disconnected environments
4
+ Cached buildpacks only ensure that a the buildpacks dependencies are cached, not your applications.
5
+
6
+ When you work with a disconnected environment, it's important to use your package manager
7
+ to 'vendor' your applications dependencies.
8
+
9
+ The specific mechanism varies between platforms. See your buildpack's documentation for 'vendoring' advice.
10
+
11
+ ## Building a cached buildpack
12
+ 1. Make sure you have fetched submodules
13
+
14
+ ```shell
15
+ git submodule update --init
16
+ ```
17
+
18
+ 1. Get the latest buildpack dependencies
19
+
20
+ ```shell
21
+ BUNDLE_GEMFILE=cf.Gemfile bundle
22
+ ```
23
+
24
+ 1. Build the buildpack
25
+
26
+ ```shell
27
+ BUNDLE_GEMFILE=cf.Gemfile bundle exec buildpack-packager [ uncached | cached ]
28
+ ```
29
+
30
+ This produces a buildpack for use on Cloud Foundry.
31
+
32
+ 'cached' generates a zip with all the dependencies cached.
33
+
34
+ 'uncached' does not include the dependencies, however it excludes some files as specified
35
+ in manifest.yml.
36
+
37
+ 1. Use in Cloud Foundry
38
+
39
+ Currently, you can only specify cached buildpacks by creating Cloud Foundry Admin Buildpacks.
40
+
41
+ This means you need admin rights. See the
42
+ [Open source admin documentation](http://docs.cloudfoundry.org/adminguide/buildpacks.html)
43
+ for more information.
44
+
45
+ Upload the buildpack to your Cloud Foundry and specify it by name:
46
+
47
+ ```shell
48
+ cf create-buildpack custom_ruby_buildpack ruby_buildpack-cached-custom.zip 1
49
+ ```
50
+
@@ -0,0 +1,14 @@
1
+ module Buildpack
2
+ class ManifestDependency
3
+ attr_reader :name, :version
4
+
5
+ def initialize(name, version)
6
+ @name = name
7
+ @version = version
8
+ end
9
+
10
+ def ==(other_object)
11
+ name == other_object.name && version == other_object.version
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,88 @@
1
+ require 'kwalify'
2
+ require 'kwalify/parser/yaml-patcher'
3
+ require 'uri'
4
+
5
+ require 'buildpack/manifest_dependency'
6
+
7
+ module Buildpack
8
+ class ManifestValidator
9
+ class ManifestValidationError < StandardError; end
10
+
11
+ SCHEMA_FILE = File.join(File.dirname(__FILE__), 'packager', 'manifest_schema.yml')
12
+
13
+ attr_reader :errors
14
+
15
+ def initialize(manifest_path)
16
+ @manifest_path = manifest_path
17
+ end
18
+
19
+ def valid?
20
+ validate
21
+ errors.empty?
22
+ end
23
+
24
+ private
25
+
26
+ def validate
27
+ schema = Kwalify::Yaml.load_file(SCHEMA_FILE)
28
+ validator = Kwalify::Validator.new(schema)
29
+ parser = Kwalify::Yaml::Parser.new(validator)
30
+ manifest_data = parser.parse_file(@manifest_path)
31
+
32
+ @errors = {}
33
+ @errors[:manifest_parser_errors] = parser.errors unless parser.errors.empty?
34
+
35
+ if manifest_data['default_versions'] && !@errors[:manifest_parser_errors]
36
+ default_version_errors = validate_default_versions(manifest_data)
37
+ @errors[:default_version] = default_version_errors unless default_version_errors.empty?
38
+ end
39
+ end
40
+
41
+ def validate_default_versions(manifest_data)
42
+ error_messages = []
43
+
44
+ default_versions = create_manifest_dependencies(manifest_data['default_versions'])
45
+ dependency_versions = create_manifest_dependencies(manifest_data['dependencies'])
46
+
47
+ error_messages += validate_no_duplicate_names(default_versions)
48
+ error_messages += validate_defaults_in_dependencies(default_versions, dependency_versions)
49
+ wrap_errors_with_common_text(error_messages)
50
+
51
+ error_messages
52
+ end
53
+
54
+ def create_manifest_dependencies(dependency_entries)
55
+ dependency_entries.map do |dependency|
56
+ ManifestDependency.new(dependency['name'], dependency['version'])
57
+ end
58
+ end
59
+
60
+ def validate_no_duplicate_names(default_versions)
61
+ default_versions_names = default_versions.map { |default_version| default_version.name }
62
+ duplicate_names = default_versions_names.find_all { |dep| default_versions_names.count(dep) > 1 }.uniq
63
+
64
+ duplicate_names.map do |name|
65
+ "- #{name} had more than one 'default_versions' entry in the buildpack manifest."
66
+ end
67
+ end
68
+
69
+ def validate_defaults_in_dependencies(default_versions, dependency_versions)
70
+ unmatched_dependencies = default_versions.select { |d| !dependency_versions.include?(d) }
71
+
72
+ unmatched_dependencies.map do |dependency|
73
+ name_version = "#{dependency.name} #{dependency.version}"
74
+
75
+ "- a 'default_versions' entry for #{name_version} was specified by the buildpack manifest, " +
76
+ "but no 'dependencies' entry for #{name_version} was found in the buildpack manifest."
77
+ end
78
+ end
79
+
80
+ def wrap_errors_with_common_text(error_messages)
81
+ if error_messages.any?
82
+ error_messages.unshift('The buildpack manifest is malformed:')
83
+ error_messages<< 'For more information, see https://docs.cloudfoundry.org/buildpacks/custom.html#specifying-default-versions'
84
+ end
85
+ end
86
+ end
87
+ end
88
+
@@ -0,0 +1,55 @@
1
+ require 'buildpack/packager/version'
2
+ require 'buildpack/packager/table_presentation'
3
+ require 'buildpack/packager/dependencies_presenter'
4
+ require 'buildpack/packager/default_versions_presenter'
5
+ require 'buildpack/packager/package'
6
+ require 'active_support/core_ext/hash/indifferent_access'
7
+ require 'open3'
8
+ require 'fileutils'
9
+ require 'tmpdir'
10
+ require 'yaml'
11
+ require 'shellwords'
12
+
13
+ module Buildpack
14
+ module Packager
15
+ class CheckSumError < StandardError; end
16
+
17
+ def self.package(options)
18
+ check_for_zip
19
+
20
+ package = Package.new(options)
21
+
22
+ Dir.mktmpdir do |temp_dir|
23
+ package.copy_buildpack_to_temp_dir(temp_dir)
24
+
25
+ package.build_dependencies(temp_dir) if options[:mode] == :cached
26
+
27
+ package.build_zip_file(temp_dir)
28
+ end
29
+
30
+ buildpack_type = options[:mode] == :cached ? "Cached" : "Uncached"
31
+ human_readable_size = `du -h #{package.zip_file_path} | cut -f1`
32
+ puts "#{buildpack_type} buildpack created and saved as #{package.zip_file_path} with a size of #{human_readable_size.strip}"
33
+
34
+ package
35
+ end
36
+
37
+ def self.list(options)
38
+ package = Package.new(options)
39
+ package.list
40
+ end
41
+
42
+ def self.defaults(options)
43
+ package = Package.new(options)
44
+ package.defaults
45
+ end
46
+
47
+ def self.check_for_zip
48
+ _, _, status = Open3.capture3('which zip')
49
+
50
+ if status.to_s.include?('exit 1')
51
+ raise "Zip is not installed\nTry: apt-get install zip\nAnd then rerun"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,38 @@
1
+ require 'terminal-table'
2
+
3
+ module Buildpack
4
+ module Packager
5
+ class DefaultVersionsPresenter < Struct.new(:default_versions)
6
+ include TablePresentation
7
+
8
+ attr_reader :default_versions
9
+
10
+ def initialize(default_versions)
11
+ default_versions = [] if default_versions.nil?
12
+ @default_versions = default_versions
13
+ end
14
+
15
+ def inspect
16
+ table = Terminal::Table.new do |table|
17
+ default_versions.sort_by do |dependency|
18
+ sort_string_for dependency
19
+ end.each do |dependency|
20
+ columns = [
21
+ dependency['name'],
22
+ sanitize_version_string(dependency['version'])
23
+ ]
24
+ table.add_row columns
25
+ end
26
+ end
27
+
28
+ table.headings = %w(name version)
29
+
30
+ table.to_s
31
+ end
32
+
33
+ def present
34
+ to_markdown(inspect)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ require 'terminal-table'
2
+
3
+ module Buildpack
4
+ module Packager
5
+ class DependenciesPresenter < Struct.new(:dependencies)
6
+ include TablePresentation
7
+
8
+ def inspect
9
+ has_modules = dependencies.any? { |dependency| dependency['modules'] }
10
+
11
+ table = Terminal::Table.new do |table|
12
+ dependencies.sort_by do |dependency|
13
+ sort_string_for dependency
14
+ end.each do |dependency|
15
+ columns = [
16
+ dependency['name'],
17
+ sanitize_version_string(dependency['version']),
18
+ dependency['cf_stacks'].sort.join(',')
19
+ ]
20
+ if has_modules
21
+ columns += [dependency.fetch('modules', []).sort.join(', ')]
22
+ end
23
+ table.add_row columns
24
+ end
25
+ end
26
+
27
+ table.headings = if has_modules
28
+ %w(name version cf_stacks modules)
29
+ else
30
+ %w(name version cf_stacks)
31
+ end
32
+
33
+ table.to_s
34
+ end
35
+
36
+ def present
37
+ to_markdown(inspect)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,67 @@
1
+ type: map
2
+ mapping:
3
+ "language":
4
+ type: str
5
+ required: yes
6
+ "url_to_dependency_map":
7
+ type: seq
8
+ required: yes
9
+ sequence:
10
+ - type: map
11
+ mapping:
12
+ "match":
13
+ type: str
14
+ required: yes
15
+ "name":
16
+ type: str
17
+ required: yes
18
+ "version":
19
+ type: text
20
+ required: yes
21
+ "dependencies":
22
+ type: seq
23
+ required: yes
24
+ sequence:
25
+ - type: map
26
+ mapping:
27
+ "name":
28
+ type: str
29
+ required: yes
30
+ "version":
31
+ type: text
32
+ required: yes
33
+ "uri":
34
+ type: str
35
+ required: yes
36
+ "modules":
37
+ type: seq
38
+ required: no
39
+ sequence:
40
+ - type: str
41
+ "md5":
42
+ type: str
43
+ required: yes
44
+ "cf_stacks":
45
+ type: seq
46
+ required: yes
47
+ sequence:
48
+ - type: str
49
+ enum: [ lucid64, cflinuxfs2 ]
50
+ "exclude_files":
51
+ type: seq
52
+ required: yes
53
+ sequence:
54
+ - type: str
55
+ "default_versions":
56
+ type: seq
57
+ required: no
58
+ sequence:
59
+ - type: map
60
+ required: yes
61
+ mapping:
62
+ "name":
63
+ type: str
64
+ required: yes
65
+ "version":
66
+ type: text
67
+ required: yes
@@ -0,0 +1,139 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'open3'
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+ require 'yaml'
6
+ require 'shellwords'
7
+ require 'buildpack/packager/zip_file_excluder'
8
+
9
+ module Buildpack
10
+ module Packager
11
+ class Package < Struct.new(:options)
12
+ def copy_buildpack_to_temp_dir(temp_dir)
13
+ FileUtils.cp_r(File.join(options[:root_dir], '.'), temp_dir)
14
+ FileUtils.cp(options[:manifest_path], File.join(temp_dir, 'manifest.yml'))
15
+ end
16
+
17
+ def build_dependencies(temp_dir)
18
+ local_cache_directory = options[:cache_dir] || "#{ENV['HOME']}/.buildpack-packager/cache"
19
+ FileUtils.mkdir_p(local_cache_directory)
20
+
21
+ dependency_dir = File.join(temp_dir, "dependencies")
22
+ FileUtils.mkdir_p(dependency_dir)
23
+
24
+ download_dependencies(manifest[:dependencies], local_cache_directory, dependency_dir)
25
+ end
26
+
27
+ def download_dependencies(dependencies, local_cache_directory, dependency_dir)
28
+ dependencies.each do |dependency|
29
+ safe_uri = uri_without_credentials(dependency['uri'])
30
+ translated_filename = uri_cache_path(safe_uri)
31
+ local_cached_file = File.expand_path(File.join(local_cache_directory, translated_filename))
32
+
33
+ if options[:force_download] || !File.exist?(local_cached_file)
34
+ puts "Downloading #{dependency['name']} version #{dependency['version']} from: #{safe_uri}"
35
+ download_file(dependency['uri'], local_cached_file)
36
+ human_readable_size = `du -h #{local_cached_file} | cut -f1`.strip
37
+ puts " Using #{dependency['name']} version #{dependency['version']} with size #{human_readable_size}"
38
+
39
+ from_local_cache = false
40
+ else
41
+ human_readable_size = `du -h #{local_cached_file} | cut -f1`.strip
42
+ puts "Using #{dependency['name']} version #{dependency['version']} from local cache at: #{local_cached_file} with size #{human_readable_size}"
43
+ from_local_cache = true
44
+ end
45
+
46
+ ensure_correct_dependency_checksum({
47
+ local_cached_file: local_cached_file,
48
+ dependency: dependency,
49
+ from_local_cache: from_local_cache
50
+ })
51
+
52
+ FileUtils.cp(local_cached_file, dependency_dir)
53
+ end
54
+ end
55
+
56
+ def build_zip_file(temp_dir)
57
+ FileUtils.rm_rf(zip_file_path)
58
+ zip_files(temp_dir, zip_file_path, manifest[:exclude_files])
59
+ end
60
+
61
+ def list
62
+ DependenciesPresenter.new(manifest['dependencies']).present
63
+ end
64
+
65
+ def defaults
66
+ DefaultVersionsPresenter.new(manifest['default_versions']).present
67
+ end
68
+
69
+ def zip_file_path
70
+ Shellwords.escape(File.join(options[:root_dir], zip_file_name))
71
+ end
72
+
73
+ private
74
+
75
+ def uri_without_credentials(uri_string)
76
+ uri = URI(uri_string)
77
+ if uri.userinfo
78
+ uri.user = "-redacted-" if uri.user
79
+ uri.password = "-redacted-" if uri.password
80
+ end
81
+ uri.to_s.sub("file:", "file://")
82
+ end
83
+
84
+ def uri_cache_path uri
85
+ uri.gsub(/[:\/\?&]/, '_')
86
+ end
87
+
88
+ def manifest
89
+ @manifest ||= YAML.load_file(options[:manifest_path]).with_indifferent_access
90
+ end
91
+
92
+ def zip_file_name
93
+ "#{manifest[:language]}_buildpack#{cached_identifier}-v#{buildpack_version}.zip"
94
+ end
95
+
96
+ def buildpack_version
97
+ File.read("#{options[:root_dir]}/VERSION").chomp
98
+ end
99
+
100
+ def cached_identifier
101
+ return '' unless options[:mode] == :cached
102
+ '-cached'
103
+ end
104
+
105
+ def ensure_correct_dependency_checksum(local_cached_file:, dependency:, from_local_cache:)
106
+ if dependency['md5'] != Digest::MD5.file(local_cached_file).hexdigest
107
+ if from_local_cache
108
+ FileUtils.rm_rf(local_cached_file)
109
+
110
+ download_file(dependency['uri'], local_cached_file)
111
+ ensure_correct_dependency_checksum({
112
+ local_cached_file: local_cached_file,
113
+ dependency: dependency,
114
+ from_local_cache: false
115
+ })
116
+ else
117
+ raise CheckSumError,
118
+ "File: #{dependency['name']}, version: #{dependency['version']} downloaded at location #{dependency['uri']}\n\tis reporting a different checksum than the one specified in the manifest."
119
+ end
120
+ else
121
+ puts " #{dependency['name']} version #{dependency['version']} matches the manifest provided md5 checksum of #{dependency['md5']}\n\n"
122
+ end
123
+ end
124
+
125
+ def download_file(url, file)
126
+ raise "Failed to download file from #{url}" unless system("curl #{url} -o #{file} -L --fail -f")
127
+ end
128
+
129
+ def zip_files(source_dir, zip_file_path, excluded_files)
130
+ excluder = ZipFileExcluder.new
131
+ manifest_exclusions = excluder.generate_manifest_exclusions excluded_files
132
+ gitfile_exclusions = excluder.generate_exclusions_from_git_files source_dir
133
+ all_exclusions = manifest_exclusions + ' ' + gitfile_exclusions
134
+ `cd #{source_dir} && zip -r #{zip_file_path} ./ #{all_exclusions}`
135
+ end
136
+ end
137
+ end
138
+ end
139
+