revamp 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,79 @@
1
+ require 'revamp'
2
+ require 'revamp/mapper/puppet-name-slugger'
3
+
4
+ # This class describes a puppet dependency
5
+ class Revamp::Model::PuppetDependency
6
+ attr_accessor :name, :version
7
+
8
+ def initialize(hash)
9
+ @name = hash['name']
10
+ versions = normalize(hash['version_requirement'])
11
+ @version = Gem::Requirement.new(versions)
12
+ end
13
+
14
+ private
15
+
16
+ def normalize(versionreq)
17
+ re = /^(([><=~]+)?\s*([\dx]+(?:\.[\dx]+(?:\.[\dx]+)?)?))(.*)/
18
+ m = re.match(versionreq.strip)
19
+ first = m[1].strip
20
+ second = m[4].strip
21
+ reqs = [first]
22
+ reqs << second unless second.empty?
23
+ reqs
24
+ end
25
+ end
26
+
27
+ # This class describes a puppet module
28
+ class Revamp::Model::PuppetModule
29
+ def self.puppet_accessor(*vars)
30
+ @attributes ||= []
31
+ @attributes.concat vars
32
+ attr_accessor(*vars)
33
+ end
34
+
35
+ class << self
36
+ attr_reader :attributes
37
+ end
38
+
39
+ attr_accessor :files
40
+ attr_accessor :metadata
41
+ attr_accessor :name
42
+
43
+ puppet_accessor :dependencies
44
+ puppet_accessor :source
45
+ puppet_accessor :author
46
+ puppet_accessor :version
47
+ puppet_accessor :license
48
+ puppet_accessor :summary
49
+ puppet_accessor :project_page
50
+
51
+ def initialize
52
+ @files = {}
53
+ @metadata = nil
54
+ @dependencies = []
55
+ end
56
+
57
+ def add_file(path, content)
58
+ @files[path] = content
59
+ end
60
+
61
+ def attributes
62
+ self.class.attributes
63
+ end
64
+
65
+ def dependencies=(deps)
66
+ @dependencies = []
67
+ deps.each do |dep|
68
+ @dependencies << Revamp::Model::PuppetDependency.new(dep)
69
+ end
70
+ end
71
+
72
+ def slugname
73
+ Revamp::Mapper::PuppetNameSlugger.new.map(name)
74
+ end
75
+
76
+ def rawname
77
+ name.split('/')[-1]
78
+ end
79
+ end
@@ -0,0 +1,78 @@
1
+ require 'revamp'
2
+ require 'zlib'
3
+ require 'json'
4
+ require 'rubygems/package'
5
+ require 'revamp/model/puppet-module'
6
+
7
+ # This class is a parser for Puppet's tarballs format
8
+ class Revamp::Parser::PuppetTarball
9
+ def initialize(tarball_file)
10
+ @tarball = tarball_file
11
+ end
12
+
13
+ def parse
14
+ model = nil
15
+ File.open(@tarball, 'rb') do |file|
16
+ Zlib::GzipReader.wrap(file) do |gz|
17
+ Gem::Package::TarReader.new(gz) do |tar|
18
+ model = Revamp::Model::PuppetModule.new
19
+ tar.each do |tarfile|
20
+ entry = Entry.new(tarfile)
21
+ parse_metadata(model, entry) if entry.metadata?
22
+ model.add_file(entry.name, entry.content) if entry.file?
23
+ end
24
+ end
25
+ end
26
+ end
27
+ normalize(model)
28
+ end
29
+
30
+ private
31
+
32
+ def normalize(model)
33
+ strip = "#{model.slugname}-#{model.version}/"
34
+ model.files = Hash[model.files.map { |file, content| [file.gsub(strip, ''), content] }]
35
+ model
36
+ end
37
+
38
+ def parse_metadata(model, entry)
39
+ data = JSON.parse(entry.content)
40
+ model.metadata = data
41
+ model.name = data['name'].tr('-', '/') if data['name']
42
+ model.attributes.each do |attr|
43
+ value = data[attr.to_s]
44
+ fail ArgumentError, "No #{attr} module metadata provided for #{name}" unless value
45
+ parse_dependencies(attr, value)
46
+ model.send(attr.to_s + '=', value)
47
+ end
48
+ end
49
+
50
+ def parse_dependencies(attr, value)
51
+ return unless attr == :dependencies
52
+ value.tap do |dependencies|
53
+ dependencies.each do |dep|
54
+ dep['version_requirement'] ||= dep['versionRequirement'] || '>= 0.0.0'
55
+ end
56
+ end
57
+ end
58
+
59
+ # An entry from tarball
60
+ class Entry
61
+ attr_accessor :content
62
+ attr_accessor :name
63
+ def initialize(tarfile)
64
+ @tarfile = tarfile
65
+ @name = tarfile.full_name
66
+ @content = nil
67
+ @content = tarfile.read unless @tarfile.directory?
68
+ end
69
+
70
+ def metadata?
71
+ !@tarfile.directory? && name.end_with?('metadata.json')
72
+ end
73
+
74
+ def file?
75
+ !content.nil?
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,28 @@
1
+ Name: <%= name %>
2
+ Version: <%= model.version %>
3
+ Release: <%= release %>
4
+ Summary: <%= model.summary %>
5
+ Source0: <%= rpm %>.tar.gz
6
+ License: <%= model.license %>
7
+ Group: Development
8
+ BuildArch: noarch
9
+ Provides: puppetmodule(<%= model.slugname %>)
10
+ <% if has_requirements %>Requires: <%= requirements %><% end %>
11
+ %description
12
+ <%= model.summary %>
13
+ %prep
14
+ %setup
15
+ %build
16
+ %install
17
+ rm -rf $RPM_BUILD_ROOT
18
+ mkdir -p ${RPM_BUILD_ROOT}/etc/puppet/modules
19
+ cp -r %{_builddir}/<%= rpm %> ${RPM_BUILD_ROOT}%{_sysconfdir}/puppet/modules
20
+ mv ${RPM_BUILD_ROOT}%{_sysconfdir}/puppet/modules/<%= rpm %> ${RPM_BUILD_ROOT}%{_sysconfdir}/puppet/modules/<%= model.rawname %>
21
+ %clean
22
+ %post
23
+ %files
24
+ %dir %{_sysconfdir}/puppet
25
+ %dir %{_sysconfdir}/puppet/modules
26
+ %dir %{_sysconfdir}/puppet/modules/<%= model.rawname %>
27
+ <% filenames.each do |file| %>%{_sysconfdir}/puppet/modules/<%= model.rawname %>/<%= file %>
28
+ <% end %>
@@ -0,0 +1,156 @@
1
+ require 'revamp'
2
+ require 'tmpdir'
3
+ require 'erb'
4
+ require 'ostruct'
5
+ require 'rbconfig'
6
+ require 'revamp/filter/puppetver2rpmreq'
7
+
8
+ # A main RPM persister
9
+ class Revamp::Persister::Rpm
10
+ def initialize
11
+ @options = nil
12
+ end
13
+
14
+ attr_accessor :options
15
+
16
+ def persist(model)
17
+ dir = File.expand_path('~')
18
+ workdir = Pathname.new(dir).join('rpmbuild')
19
+ builder = Builder.new(model, workdir, options)
20
+ builder.make_structure
21
+ builder.write_spec
22
+ builder.write_sources
23
+ produced = builder.produce
24
+ builder.cleanup if options[:cleanup]
25
+ produced
26
+ end
27
+
28
+ # A command line executor for command line
29
+ class CommandLine
30
+ class << self
31
+ def execute(command, directory, verbose)
32
+ Revamp.logger.debug("Executing: '#{command}' in directory: '#{directory}'")
33
+ out = '/dev/null'
34
+ out = $stdout if verbose
35
+ pid = Process.spawn(command, chdir: directory, out: out, err: out)
36
+ Process.wait pid
37
+ $? # rubocop:disable SpecialGlobalVars
38
+ end
39
+ end
40
+ end
41
+
42
+ # A builder for RPM's packages
43
+ class Builder
44
+ SOURCES = 'SOURCES'
45
+ SPECS = 'SPECS'
46
+ RPMS = 'RPMS'
47
+ SELFDIR = Pathname.new(__FILE__).dirname
48
+ ATTRS = [
49
+ :name, :version, :filename, :rpm,
50
+ :model, :tmpdir, :release, :filenames,
51
+ :specfile, :has_requirements,
52
+ :requirements, :options
53
+ ]
54
+ ATTRS.each { |attr| attr_accessor(attr) }
55
+
56
+ def initialize(model, dir, options)
57
+ @options = options
58
+ @name = "puppetmodule_#{model.slugname}"
59
+ @release = options[:release]
60
+ @version = model.version
61
+ @filename = "#{name}-#{version}-#{release}.noarch.rpm"
62
+ @rpm = "#{name}-#{version}"
63
+ @specfile = rpm + '.spec'
64
+ @model = model
65
+ @tmpdir = dir
66
+ @filenames = model.files.keys
67
+ @has_requirements = model.dependencies.any?
68
+ @requirements = configure_rpm_requirements_line
69
+ end
70
+
71
+ def configure_rpm_requirements_line
72
+ req = []
73
+ model.dependencies.each do |dep|
74
+ req += Revamp::Filter::PuppetVerToRpmReq.new.filter(dep)
75
+ end
76
+ req.join(', ')
77
+ end
78
+
79
+ def make_structure
80
+ FileUtils.mkdir_p(tmpdir.join(SOURCES))
81
+ FileUtils.mkdir_p(tmpdir.join(SPECS))
82
+ end
83
+
84
+ def erbize(template, vars)
85
+ values = OpenStruct.new(vars).instance_eval { binding }
86
+ ERB.new(template).result(values)
87
+ end
88
+
89
+ def write_spec
90
+ tpl = SELFDIR.join('rpm-spec.erb')
91
+ values = Hash[ATTRS.map { |key| [key, send(key)] }]
92
+ spec = erbize(tpl.read, values)
93
+ File.open(specfile_path, 'w') { |file| file.write(spec) }
94
+ end
95
+
96
+ def write_sources
97
+ pathsufix = Pathname.new(rpm)
98
+ File.open(sources_tgz_path, 'wb') do |tgz|
99
+ Zlib::GzipWriter.wrap(tgz) do |gz|
100
+ Gem::Package::TarWriter.new(gz) do |tar|
101
+ model.files.each do |file, content|
102
+ full = pathsufix.join(file).to_s
103
+ tar.add_file_simple(full, 0644, content.length) { |io| io.write(content) }
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def sources_tgz_path
111
+ sources = tmpdir.join(SOURCES)
112
+ sources.join("#{rpm}.tar.gz")
113
+ end
114
+
115
+ def specfile_path
116
+ tmpdir.join(SPECS).join(specfile)
117
+ end
118
+
119
+ def target
120
+ outdir = options[:outdir]
121
+ Pathname.new(outdir).join(filename)
122
+ end
123
+
124
+ def produce
125
+ Revamp.logger.info("Converting to RPM package #{target}...")
126
+ cmd = "rpmbuild -ba #{SPECS}/#{specfile}"
127
+ verbose = options[:verbose]
128
+ ret = Revamp::Persister::Rpm::CommandLine.execute(cmd, tmpdir, verbose)
129
+ fail "RPM Build failed with retcode = #{ret.exitstatus}" unless ret.success?
130
+ move_produced
131
+ target
132
+ end
133
+
134
+ def move_produced
135
+ produced = tmpdir.join(RPMS).join('noarch').join(filename)
136
+ Revamp.logger.debug("Produced RPM in build dir: #{produced}")
137
+ FileUtils.mv(produced, target)
138
+ end
139
+
140
+ def cleanup
141
+ arch = RbConfig::CONFIG['arch'].gsub('-linux', '')
142
+ cleanup_files [
143
+ sources_tgz_path, specfile_path, tmpdir.join('BUILD').join(rpm),
144
+ tmpdir.join('BUILDROOT').join("#{rpm}-#{release}.#{arch}")
145
+ ]
146
+ end
147
+
148
+ def cleanup_files(files)
149
+ Revamp.logger.debug("Files to be cleaned up: #{files}")
150
+ readable = files.reject { |path| !path.readable? }
151
+ readable.each do |path|
152
+ path.directory? ? FileUtils.rm_r(path) : path.unlink
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,35 @@
1
+ # Top level module for Revamp
2
+ module Revamp
3
+ # Prepare version
4
+ #
5
+ # @param desired [String] a desired version
6
+ # @return [String] a prepared version
7
+ def self.version_prep(desired)
8
+ version = desired
9
+ if desired.match(/[^0-9\.]+/)
10
+ git = `git describe --tags --dirty --always`
11
+ version += '.' + git.tr('-', '.')
12
+ end
13
+ version.strip
14
+ end
15
+
16
+ # Version for Herald
17
+ VERSION = version_prep '1.0.0'
18
+ # Lincense for Herald
19
+ LICENSE = 'Apache-2.0'
20
+ # Project name
21
+ NAME = 'Revamp'
22
+ # Package (gem) for Herald
23
+ PACKAGE = 'revamp'
24
+ # A summary info
25
+ SUMMARY = 'Converts puppet module file to valid RPM or DEB package'
26
+ # A homepage for Herald
27
+ HOMEPAGE = 'https://github.com/coi-gov-pl/gem-revamp'
28
+ # A description info
29
+ DESCRIPTION = <<-eos
30
+ This module can convert standard puppet module file in form of tarball to
31
+ valid RPM or DEB package with all dependencies as references to other
32
+ system packages. The dependencies can be packaged inside the final system
33
+ package or just referenced as dependencies.
34
+ eos
35
+ end
data/lib/revamp.rb ADDED
@@ -0,0 +1,61 @@
1
+ require 'logger'
2
+
3
+ begin
4
+ require 'pry'
5
+ rescue LoadError # rubocop:disable Lint/HandleExceptions
6
+ # Do nothing here
7
+ end
8
+
9
+ # Top level module for Revamp
10
+ module Revamp
11
+ @logger = Logger.new($stdout)
12
+ @logger.formatter = proc { |severity, _datetime, _progname, msg| "#{severity}: #{msg}\n" }
13
+ class << self
14
+ # Logger for CLI interface
15
+ # @return [Logger] logger for CLI
16
+ attr_reader :logger
17
+
18
+ # Reports a bug in desired format
19
+ #
20
+ # @param ex [Exception] an exception that was thrown
21
+ # @return [Hash] a hash with info about bug to be displayed to user
22
+ def bug(ex)
23
+ file = Tempfile.new(['revamp-bug', '.log'])
24
+ filepath = file.path
25
+ file.close
26
+ file.unlink
27
+ message = "v#{Revamp::VERSION}-#{ex.class}: #{ex.message}"
28
+ contents = message + "\n\n" + ex.backtrace.join("\n") + "\n"
29
+ File.write(filepath, contents)
30
+ bugo = {
31
+ message: message,
32
+ homepage: Revamp::HOMEPAGE,
33
+ bugfile: filepath,
34
+ help: "Please report this bug to #{Revamp::HOMEPAGE} by passing contents of bug file: #{filepath}"
35
+ }
36
+ bugo
37
+ end
38
+ end
39
+ end
40
+
41
+ # A module for modeles
42
+ module Revamp::Model
43
+ end
44
+
45
+ # A module for mapper
46
+ module Revamp::Mapper
47
+ end
48
+
49
+ # A module for persister
50
+ module Revamp::Persister
51
+ end
52
+
53
+ # Parser module
54
+ module Revamp::Parser
55
+ end
56
+
57
+ # A module for filters of persiters
58
+ module Revamp::Filter
59
+ end
60
+
61
+ require 'revamp/version'
data/revamp.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'revamp/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = Revamp::PACKAGE
8
+ spec.version = Revamp::VERSION
9
+ spec.authors = [
10
+ 'Centralny Ośrodek Informatyki',
11
+ 'Suszyński Krzysztof'
12
+ ]
13
+ spec.email = ['krzysztof.suszynski@coi.gov.pl']
14
+ spec.summary = Revamp::SUMMARY
15
+ spec.description = Revamp::DESCRIPTION
16
+ spec.homepage = Revamp::HOMEPAGE
17
+ spec.license = Revamp::LICENSE
18
+
19
+ spec.files = `git ls-files -z`.split("\x0")
20
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_development_dependency 'bundler'
25
+ spec.add_development_dependency 'rake'
26
+ spec.add_runtime_dependency 'micro-optparse', '~> 1.2.0'
27
+
28
+ spec.required_ruby_version = '>= 1.9'
29
+ end
@@ -0,0 +1,5 @@
1
+ RSpec::Matchers.define :be_readable_file do
2
+ match do |actual|
3
+ File.readable?(actual)
4
+ end
5
+ end
@@ -0,0 +1,49 @@
1
+ require 'rspec/its'
2
+
3
+ begin
4
+ gem 'simplecov'
5
+ require 'simplecov'
6
+ formatters = []
7
+ formatters << SimpleCov::Formatter::HTMLFormatter
8
+
9
+ begin
10
+ gem 'coveralls'
11
+ require 'coveralls'
12
+ formatters << Coveralls::SimpleCov::Formatter if ENV['TRAVIS']
13
+ rescue Gem::LoadError
14
+ # do nothing
15
+ end
16
+
17
+ begin
18
+ gem 'codeclimate-test-reporter'
19
+ require 'codeclimate-test-reporter'
20
+ formatters << CodeClimate::TestReporter::Formatter if (ENV['TRAVIS'] and ENV['CODECLIMATE_REPO_TOKEN'])
21
+ rescue Gem::LoadError
22
+ # do nothing
23
+ end
24
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[*formatters]
25
+ SimpleCov.start do
26
+ add_filter "/spec/"
27
+ add_filter "/.vendor/"
28
+ add_filter "/vendor/"
29
+ add_filter "/gems/"
30
+ minimum_coverage 95
31
+ refuse_coverage_drop
32
+ end
33
+ rescue Gem::LoadError
34
+ # do nothing
35
+ end
36
+
37
+ begin
38
+ gem 'pry'
39
+ require 'pry'
40
+ rescue Gem::LoadError
41
+ # do nothing
42
+ end
43
+
44
+ RSpec.configure do |c|
45
+ c.tty = true unless ENV['JENKINS_URL'].nil?
46
+ c.mock_with :rspec do |mock|
47
+ mock.syntax = [:expect]
48
+ end
49
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+ require 'revamp/cli'
3
+ require 'revamp/application'
4
+ require 'stringio'
5
+
6
+ describe Revamp::CLI do
7
+ let(:instance) { described_class.new }
8
+ describe '.run!' do
9
+ before :each do
10
+ @level = Revamp.logger.level
11
+ Revamp.logger.level = 100
12
+ end
13
+ after :each do
14
+ Revamp.logger.level = @level
15
+ end
16
+ context 'while ARGV is equal to' do
17
+ let (:argv) { self.class.description.split(/\s+/) }
18
+ subject { instance.run!(argv) }
19
+ describe '--invalid-option' do
20
+ it do
21
+ expect { subject }.to raise_error(SystemExit, 'exit') { |error|
22
+ expect(error).not_to be_success
23
+ }
24
+ end
25
+ end
26
+ context 'while catching STDOUT' do
27
+ before :each do
28
+ @stringio = StringIO.new
29
+ $stdout = @stringio
30
+ end
31
+ after :each do
32
+ $stdout = STDOUT
33
+ end
34
+ describe '--help' do
35
+ it do
36
+ begin
37
+ subject
38
+ rescue SystemExit
39
+ end
40
+ expect(@stringio.string).to include('Revamp v', 'Usage:', '-h, --help')
41
+ end
42
+ it do
43
+ expect { subject }.to raise_error(SystemExit, 'exit') { |error|
44
+ expect(error).to be_success
45
+ }
46
+ end
47
+ end
48
+ describe '--version' do
49
+ it do
50
+ begin
51
+ subject
52
+ rescue SystemExit
53
+ end
54
+ expect(@stringio.string).to match(/^\d+\.\d+\.\d+/)
55
+ end
56
+ it do
57
+ expect { subject }.to raise_error(SystemExit, 'exit') { |error|
58
+ expect(error).to be_success
59
+ }
60
+ end
61
+ end
62
+ end
63
+
64
+ context 'mocking to return' do
65
+ before :each do
66
+ app = Revamp::Application.new({})
67
+ expect(app).to receive(:run!).and_return(:mock_return)
68
+ expect(Revamp::Application).to receive(:new).and_return(app)
69
+ end
70
+ describe '-f spec/fixtures/coi-sample-0.1.1.tar.gz' do
71
+ it { expect(subject).to eq(0) }
72
+ end
73
+ end
74
+ context 'mocking to raise error' do
75
+ before :each do
76
+ app = Revamp::Application.new({})
77
+ expect(app).to receive(:run!).and_raise(StandardError, 'mock-ex')
78
+ expect(Revamp::Application).to receive(:new).and_return(app)
79
+ end
80
+ describe '-f spec/fixtures/coi-sample-0.1.1.tar.gz' do
81
+ it { expect(subject).to eq(1) }
82
+ end
83
+ end
84
+ context 'performing real operation' do
85
+ before :each do
86
+ require 'revamp/persister/rpm'
87
+ ret = double(success?: true)
88
+ expect(Revamp::Persister::Rpm::CommandLine).to receive(:execute).and_return(ret)
89
+ expect(FileUtils).to receive(:mv)
90
+ end
91
+ describe '-f spec/fixtures/coi-sample-0.1.1.tar.gz -o /tmp' do
92
+ it { expect(subject).to eq(0) }
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ describe '.parse' do
99
+ let (:argv) { eval(self.class.description) }
100
+ subject { instance.send(:parse, argv) }
101
+ describe([].inspect) do
102
+ it { expect { subject }.to raise_error(ArgumentError, /You must pass filenames with `-f`/) }
103
+ end
104
+ describe(['-f', 'abc-lib-1.0.0.tar.gz'].inspect) do
105
+ it { expect { subject }.to raise_error(ArgumentError, 'Can\'t read file given: abc-lib-1.0.0.tar.gz') }
106
+ end
107
+ describe(['-f', 'spec/fixtures/coi-sample-0.1.1.tar.gz', '-o', Dir.tmpdir].inspect) do
108
+ it do
109
+ expect(subject).to eq({
110
+ release: "1",
111
+ epoch: "6",
112
+ outdir: "/tmp",
113
+ filenames: ["spec/fixtures/coi-sample-0.1.1.tar.gz"],
114
+ verbose: false,
115
+ cleanup: true
116
+ })
117
+ end
118
+ end
119
+ end
120
+ end