right_publish 0.2.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.
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --colour
2
+ --format=nested
3
+ --backtrace
4
+ --debugger
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "builder"
4
+ gem "fog", "~> 1.9"
5
+ gem "trollop", "~> 2.0"
6
+
7
+ # Gems used to test and develop RightPublish
8
+ group :development do
9
+ gem "rake", "~> 0.9"
10
+ gem "jeweler", "~> 1.8.3"
11
+ gem "right_develop", "~> 1.0",
12
+ :git => "git@github.com:rightscale/right_develop.git",
13
+ :branch => "master"
14
+ gem "rdoc", ">= 2.4.2"
15
+ gem "rspec", "~> 2.0"
16
+ gem "flexmock", "~> 0.9"
17
+ gem "simplecov"
18
+ gem "ruby-debug", ">= 0.10", :platforms => :ruby_18
19
+ gem "ruby-debug19", ">= 0.11.6", :platforms => :ruby_19
20
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,114 @@
1
+ GIT
2
+ remote: git@github.com:rightscale/right_develop.git
3
+ revision: 90a8fde986145756f341088130c6e1aac4759d68
4
+ branch: master
5
+ specs:
6
+ right_develop (1.1.0)
7
+ actionpack (>= 2.3.0, < 4.0)
8
+ builder (~> 3.0)
9
+ cucumber (~> 1.0)
10
+ rake (>= 0.8.7, < 0.10)
11
+ right_support (~> 2.0)
12
+ rspec (>= 1.3, < 3.0)
13
+ trollop (>= 1.0, < 3.0)
14
+
15
+ GEM
16
+ remote: http://rubygems.org/
17
+ specs:
18
+ actionpack (2.3.18)
19
+ activesupport (= 2.3.18)
20
+ rack (~> 1.1.0)
21
+ activesupport (2.3.18)
22
+ archive-tar-minitar (0.5.2)
23
+ builder (3.2.0)
24
+ columnize (0.3.6)
25
+ cucumber (1.2.5)
26
+ builder (>= 2.1.2)
27
+ diff-lcs (>= 1.1.3)
28
+ gherkin (~> 2.11.7)
29
+ multi_json (~> 1.3)
30
+ diff-lcs (1.2.3)
31
+ excon (0.20.1)
32
+ flexmock (0.9.0)
33
+ fog (1.10.1)
34
+ builder
35
+ excon (~> 0.20)
36
+ formatador (~> 0.2.0)
37
+ mime-types
38
+ multi_json (~> 1.0)
39
+ net-scp (~> 1.1)
40
+ net-ssh (>= 2.1.3)
41
+ nokogiri (~> 1.5.0)
42
+ ruby-hmac
43
+ formatador (0.2.4)
44
+ gherkin (2.11.8)
45
+ multi_json (~> 1.3)
46
+ git (1.2.5)
47
+ jeweler (1.8.4)
48
+ bundler (~> 1.0)
49
+ git (>= 1.2.5)
50
+ rake
51
+ rdoc
52
+ json (1.7.7)
53
+ linecache (0.46)
54
+ rbx-require-relative (> 0.0.4)
55
+ linecache19 (0.5.12)
56
+ ruby_core_source (>= 0.1.4)
57
+ mime-types (1.22)
58
+ multi_json (1.7.2)
59
+ net-scp (1.1.0)
60
+ net-ssh (>= 2.6.5)
61
+ net-ssh (2.6.7)
62
+ nokogiri (1.5.9)
63
+ rack (1.1.6)
64
+ rake (0.9.6)
65
+ rbx-require-relative (0.0.9)
66
+ rdoc (4.0.1)
67
+ json (~> 1.4)
68
+ right_support (2.6.17)
69
+ rspec (2.13.0)
70
+ rspec-core (~> 2.13.0)
71
+ rspec-expectations (~> 2.13.0)
72
+ rspec-mocks (~> 2.13.0)
73
+ rspec-core (2.13.1)
74
+ rspec-expectations (2.13.0)
75
+ diff-lcs (>= 1.1.3, < 2.0)
76
+ rspec-mocks (2.13.1)
77
+ ruby-debug (0.10.4)
78
+ columnize (>= 0.1)
79
+ ruby-debug-base (~> 0.10.4.0)
80
+ ruby-debug-base (0.10.4)
81
+ linecache (>= 0.3)
82
+ ruby-debug-base19 (0.11.25)
83
+ columnize (>= 0.3.1)
84
+ linecache19 (>= 0.5.11)
85
+ ruby_core_source (>= 0.1.4)
86
+ ruby-debug19 (0.11.6)
87
+ columnize (>= 0.3.1)
88
+ linecache19 (>= 0.5.11)
89
+ ruby-debug-base19 (>= 0.11.19)
90
+ ruby-hmac (0.4.0)
91
+ ruby_core_source (0.1.5)
92
+ archive-tar-minitar (>= 0.5.2)
93
+ simplecov (0.7.1)
94
+ multi_json (~> 1.0)
95
+ simplecov-html (~> 0.7.1)
96
+ simplecov-html (0.7.1)
97
+ trollop (2.0)
98
+
99
+ PLATFORMS
100
+ ruby
101
+
102
+ DEPENDENCIES
103
+ builder
104
+ flexmock (~> 0.9)
105
+ fog (~> 1.9)
106
+ jeweler (~> 1.8.3)
107
+ rake (~> 0.9)
108
+ rdoc (>= 2.4.2)
109
+ right_develop (~> 1.0)!
110
+ rspec (~> 2.0)
111
+ ruby-debug (>= 0.10)
112
+ ruby-debug19 (>= 0.11.6)
113
+ simplecov
114
+ trollop (~> 2.0)
data/README.rdoc ADDED
@@ -0,0 +1,113 @@
1
+ = RightPublish
2
+
3
+ = DESCRIPTION
4
+
5
+ == Synopsis
6
+
7
+ RightPublish facilitates the management of remote package repositories. It is in
8
+ essense a wrapper around package repository management tools such as YUM and
9
+ RubyGems allowing one to add/remove/replace packages stored in a repository
10
+ residing in a remote storage location, typically Amazon's S3. The current
11
+ incantation of RightPublish can manage RubyGem, YUM (RPM) and APT (DEB) pacakge
12
+ repositories.
13
+
14
+ == Interface
15
+
16
+ RightPublish is used as a command line tool and controlled via command line options
17
+ and a YAML configuration file known as a RightPublish <i>profile</i>.
18
+
19
+ === Profile Format
20
+
21
+ ###########################################################
22
+ # RightPublish Profile
23
+ #
24
+ # + Attribute is required, example shown
25
+ # * Attribute is optional, no default; example shown
26
+ # - Attribute is optional, default shown
27
+ #
28
+ # Values of the form "@@...@@" indicate an environment
29
+ # variable should be substituted in place of the value.
30
+ # These substitution sequences can appear as the entire
31
+ # value of an attribute, or they can appear anywhere in
32
+ # the attribute with static text surrounding them.
33
+ #
34
+ :verbose: # -: false
35
+ :apt_repo:
36
+ :dists: # *: [ woody, sid, etch, jaunty, lucid ]
37
+ :auto: # -: true
38
+ :subdir: # -: apt/
39
+ :gpg_key_id: # *: 9A917D05
40
+ :gpg_password: # *: @@GPG_PASSWORD@@
41
+ :gem_repo:
42
+ :subdir: # -: gems/
43
+ :yum_repo:
44
+ :dists: # *: el: ['6']
45
+ :epel: # -: 1
46
+ :subdir: # -: yum/
47
+ :gpg_key_id: # *: 9A917D05
48
+ :gpg_password: # *: @@GPG_PASSWORD@@
49
+ :local_storage:
50
+ :cache_dir # -: ~/.rp_cache
51
+ :remote_storage:
52
+ :storage_provider: # -: AWS
53
+ :access_id: # *: @@AWS_ACCESS_KEY_ID@@
54
+ :access_key: # *: @@AWS_SECRET_ACCESS_KEY@@
55
+ :remote_path: # +: rightlink-testing
56
+ #
57
+ # End of RightPublish Profile
58
+ ###########################################################
59
+
60
+ === Command Line Usage
61
+
62
+ RightPublish can manage a YUM/APT/RubyGem repository in remote storage, e.g. S3.
63
+ If autosigning packages import the private key beforehand with gpg --import. If
64
+ no gpg_password config key is supplied but a gpg_key_id is it should prompt you
65
+ for a password.
66
+
67
+ Usage:
68
+ right_publish [global_options] <command> [cmd_options]
69
+ commands:
70
+ fetch
71
+ publish
72
+ store
73
+ global options:
74
+ --profile, -p <s>: Publish profile
75
+ --repo-type, -r <s>: Repository type: ["apt", "gem", "yum"]
76
+ --dist, -d: Target dist: ["el/6", "precise"]
77
+ --verbose, -v: Verbose output
78
+ --version, -e: Print version and exit
79
+ --help, -h: Show this message
80
+
81
+
82
+ == Supported Configuration
83
+
84
+ RightPublish has been tested on EL6, Ubuntu 12.10 and ArchLinux (as of March `13).
85
+
86
+ == Work in Progress
87
+
88
+ RightPublish is work in progress, expect more documentation and examples in the near future.
89
+
90
+ = LICENSE
91
+
92
+ <b>RightPublish</b>
93
+
94
+ Copyright:: Copyright (c) 2013 RightScale, Inc.
95
+
96
+ Permission is hereby granted, free of charge, to any person obtaining
97
+ a copy of this software and associated documentation files (the
98
+ 'Software'), to deal in the Software without restriction, including
99
+ without limitation the rights to use, copy, modify, merge, publish,
100
+ distribute, sublicense, and/or sell copies of the Software, and to
101
+ permit persons to whom the Software is furnished to do so, subject to
102
+ the following conditions:
103
+
104
+ The above copyright notice and this permission notice shall be
105
+ included in all copies or substantial portions of the Software.
106
+
107
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
108
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
109
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
110
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
111
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
112
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
113
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ # -*-ruby-*-
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+
5
+ require 'rake'
6
+ require 'rdoc/task'
7
+ require 'rubygems/package_task'
8
+
9
+ require 'rake/clean'
10
+ require 'rspec/core/rake_task'
11
+
12
+ require 'right_develop/ci/rake_task'
13
+
14
+ desc "Run unit tests"
15
+ task :default => :spec
16
+
17
+ desc "Run unit tests"
18
+ RSpec::Core::RakeTask.new do |t|
19
+ t.pattern = Dir['**/*_spec.rb']
20
+ end
21
+
22
+ desc 'Generate documentation for the right_publish gem.'
23
+ Rake::RDocTask.new(:rdoc) do |rdoc|
24
+ rdoc.rdoc_dir = 'doc'
25
+ rdoc.title = 'RightPublish'
26
+ rdoc.options << '--line-numbers' << '--inline-source'
27
+ rdoc.rdoc_files.include('README.rdoc')
28
+ rdoc.rdoc_files.include('lib/**/*.rb')
29
+ rdoc.rdoc_files.exclude('features/**/*')
30
+ rdoc.rdoc_files.exclude('spec/**/*')
31
+ end
32
+
33
+ require 'jeweler'
34
+ Jeweler::Tasks.new do |gem|
35
+ # gem is a Gem::Specification; see http://docs.rubygems.org/read/chapter/20 for more options
36
+ gem.name = "right_publish"
37
+ gem.homepage = "https://github.com/rightscale/right_publish"
38
+ gem.license = "Proprietary"
39
+ gem.summary = %Q{Package publishing and indexing tool}
40
+ gem.description = %Q{A tool for maintaining S3-based DEB, GEM and RPM packages.}
41
+ gem.email = "support@rightscale.com"
42
+ gem.authors = ['Brian Szmyd', 'Tony Spataro']
43
+ end
44
+ Jeweler::RubygemsDotOrgTasks.new
45
+
46
+ CLEAN.include('pkg')
47
+
48
+ RightDevelop::CI::RakeTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
data/bin/right_publish ADDED
@@ -0,0 +1,8 @@
1
+ #! /usr/bin/env ruby
2
+ #
3
+ #
4
+
5
+ $: << File.expand_path("../../lib",__FILE__)
6
+
7
+ require 'right_publish'
8
+ RightPublish::Main.run()
@@ -0,0 +1,96 @@
1
+ require 'singleton'
2
+ require 'yaml'
3
+
4
+ module RightPublish
5
+
6
+ class Profile
7
+
8
+ # Configuration defaults
9
+ # =============
10
+ DEFAULT_VERBOSE = false
11
+ # =============
12
+
13
+ attr_accessor :settings
14
+ include Singleton
15
+
16
+ def self.config()
17
+ return Profile.instance.settings
18
+ end
19
+
20
+ def self.log(s, level=:info)
21
+ puts(s) if Profile.config[:verbose] || level != :debug
22
+ end
23
+
24
+ def load(path)
25
+ begin
26
+ if path && File.exists?(path)
27
+ begin
28
+ @settings ||= Profile.symbolize_profile(YAML.load_file(path))
29
+ validate_profile
30
+ rescue Exception => e
31
+ raise LoadError.new("Bad Format: #{path}\n#{e}")
32
+ end
33
+ else
34
+ raise LoadError.new("Missing Profile")
35
+ end
36
+ rescue LoadError => e
37
+ raise LoadError.new("#{e}")
38
+ end
39
+ nil
40
+ end
41
+
42
+ def register_section(section_key, section_options)
43
+ @config_map ||= {}
44
+ @config_map[section_key] ||= section_options
45
+ end
46
+
47
+ private
48
+
49
+ def self.symbolize_profile(args)
50
+ if args.is_a? Hash
51
+ Hash[ args.collect do |key, value|
52
+ [key.to_sym, symbolize_profile(value)]
53
+ end ]
54
+ else
55
+ args
56
+ end
57
+ end
58
+
59
+ SUBSTITUTION = /@@(\w+)@@/
60
+
61
+ def check_section(section, attrs)
62
+ provider = (attrs[:provider] && attrs[:provider]) || section
63
+ raise LoadError.new("Unknown provider #{attrs[:provider]} for: #{section}") unless @config_map[provider]
64
+
65
+ @settings[section] ||= {}
66
+ @config_map[provider].each_pair do |attr, default|
67
+ setting = @settings[section][attr]
68
+
69
+ if setting.nil? && (default != :attr_optional)
70
+ if default == :attr_needed
71
+ raise LoadError.new("Missing attribute #{section}:#{attr}")
72
+ end
73
+ @settings[section][attr] = default
74
+ end
75
+
76
+ # Perform environment-variable substitition if the setting is a leaf node
77
+ if setting.is_a?(String)
78
+ while match = SUBSTITUTION.match(setting)
79
+ if ENV.key?(match[1])
80
+ setting = setting.gsub(match[0], ENV[match[1]])
81
+ else
82
+ raise LoadError.new("Missing environment variable #{match[1]}.")
83
+ end
84
+ end
85
+ end
86
+
87
+ @settings[section][attr] = setting
88
+ end
89
+ end
90
+
91
+ def validate_profile()
92
+ @settings[:verbose] = DEFAULT_VERBOSE unless @settings[:verbose]
93
+ @settings.each_pair { |section, attrs| check_section(section, attrs) if attrs.is_a? Hash }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,159 @@
1
+ require 'right_publish/profile'
2
+ require 'right_publish/storage'
3
+ require 'pty'
4
+ require 'expect'
5
+
6
+ module RightPublish
7
+
8
+ class RepoManager
9
+ @@repository_type_regex = /\A(\w+)_repo/
10
+ @@repository_table = {}
11
+
12
+ def self.get_repository(type)
13
+ @@repository_table[type].new(type) if @@repository_table[type]
14
+ end
15
+
16
+ def self.repo_types()
17
+ type_hash = {}
18
+ @@repository_table.each_key { |k| type_hash[@@repository_type_regex.match(k.to_s)[1]] = k }
19
+ type_hash
20
+ end
21
+
22
+ def self.register_repo(repo_type)
23
+ repo_key = repo_type::REPO_KEY
24
+
25
+ if repo_type.respond_to?(:new) && @@repository_type_regex.match(repo_key.to_s)
26
+ @@repository_table[repo_key] = repo_type
27
+ RightPublish::Profile.instance.register_section(repo_key, repo_type::REPO_OPTIONS)
28
+ else
29
+ raise TypeError
30
+ end
31
+
32
+ nil
33
+ end
34
+ end
35
+
36
+ module Repo
37
+ def initialize(option_key)
38
+ @repository_type ||= option_key
39
+ end
40
+
41
+ def fetch()
42
+ Profile.log("Fetching latest #{repo_human_name(@repository_type)} data...")
43
+ begin
44
+ sync_dirs(
45
+ get_storage(Profile.config[:remote_storage][:provider]),
46
+ get_storage(Profile.config[:local_storage][:provider]),
47
+ Profile.config[@repository_type][:subdir] )
48
+ rescue Exception => e
49
+ RightPublish::Profile.log("Could not synchronize storage:\n\t#{e}", :error)
50
+ raise RuntimeError, "fetch from remote failed."
51
+ end
52
+ end
53
+
54
+ def store()
55
+ Profile.log("Commiting local #{repo_human_name(@repository_type)} data...")
56
+ begin
57
+ sync_dirs(
58
+ get_storage(Profile.config[:local_storage][:provider]),
59
+ get_storage(Profile.config[:remote_storage][:provider]),
60
+ Profile.config[@repository_type][:subdir] )
61
+ rescue Exception => e
62
+ RightPublish::Profile.log("Could not sychronize storage:\n\t#{e}", :error)
63
+ raise RuntimeError, "store to remote failed."
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def build_glob(ext)
70
+ # Transform array into glob compliant pattern
71
+ '{'.concat Array(ext).join(',').concat '}'
72
+ end
73
+
74
+ def do_in_subdir(subdir)
75
+ full_path = File.expand_path(File.join(Profile.config[:local_storage][:cache_dir], subdir))
76
+ FileUtils.mkdir_p full_path
77
+
78
+ Dir.chdir(full_path) { yield }
79
+ end
80
+
81
+ def get_pkg_list(file_or_dir, ext=nil)
82
+ pkg_list = []
83
+ file_or_dir = Array(file_or_dir)
84
+ file_or_dir.each do |path|
85
+ path = path.gsub("\\", "/")
86
+ pkg_list << if File.directory?(path)
87
+ glob_filter = (ext && "*.#{build_glob(ext)}") || "*"
88
+ Dir.glob(File.join(path, glob_filter))
89
+ else
90
+ path.split(',')
91
+ end
92
+ end
93
+ pkg_list.flatten!
94
+
95
+ fail("No packages found") if pkg_list.empty?
96
+ pkg_list.each do |pkg_path|
97
+ fail("\"#{pkg_path}\" does not appear to be a valid package for the repository.") \
98
+ unless File.file?(pkg_path) && Array(ext).any? { |e| pkg_path.end_with?(e) }
99
+ yield pkg_path if block_given?
100
+ end if ext
101
+
102
+ pkg_list
103
+ end
104
+
105
+ # For automation, we want to send the password, however gpg uses getpass
106
+ # c function, which interacts directly with /dev/pty instead of stdin/stdout
107
+ # to hide the password as its typed in. So, we need to allocate a pty.
108
+ def shellout_with_password(cmd)
109
+ password = repo_config[:gpg_password]
110
+ raise Exception, ":gpg_password must be supplied when signing packages" unless password
111
+
112
+ begin
113
+ PTY.spawn(cmd) do |stdin,stdout,pid|
114
+ while output = stdin.expect(/pass.?phrase/i, 2)
115
+ stdout.puts(password)
116
+ Profile.log(output, :debug)
117
+ end
118
+ Process.wait(pid)
119
+ end
120
+ status = $?
121
+ rescue PTY::ChildExited => e
122
+ status = e.status
123
+ end
124
+ return status.success?
125
+ end
126
+
127
+ def get_storage(provider)
128
+ type = RightPublish::StorageManager.storage_types[provider]
129
+ RightPublish::StorageManager.get_storage(type)
130
+ end
131
+
132
+ def install_file(file, dest)
133
+ Profile.log("#{file} => #{dest}")
134
+ local_dir = get_storage(Profile.config[:local_storage][:provider]).get_directories
135
+ File.open(file, "rb") { |chunk| local_dir.files.create(:key=>File.join(dest, File.basename(file)), :body=>chunk) }
136
+ end
137
+
138
+ def prune_all(glob_pattern)
139
+ Profile.log("Pruning: #{glob_pattern}")
140
+ Dir.glob(glob_pattern) { |f| File.unlink(f) }
141
+ end
142
+
143
+ def repo_config()
144
+ Profile.config[@repository_type]
145
+ end
146
+
147
+ def repo_human_name(type)
148
+ type.to_s.sub(/_/,' ')
149
+ end
150
+
151
+ def sync_dirs(src, dest, subdir='')
152
+ RightPublish::Storage.sync_dirs(src, dest, :subdir=>subdir, :sweep=>true)
153
+ end
154
+ end
155
+ end
156
+
157
+ require 'right_publish/repos/apt'
158
+ require 'right_publish/repos/gem'
159
+ require 'right_publish/repos/yum'