buildpack-packager 2.3.4

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 (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
+