right_publish 0.3.0 → 0.4.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/Gemfile CHANGED
@@ -2,6 +2,8 @@ source "http://rubygems.org"
2
2
 
3
3
  gem "builder"
4
4
  gem "fog", "~> 1.9"
5
+ gem "excon", "<= 0.25.3" # We do not depend directly on excon, but fog does, and > 0.25 is incompatible with fog
6
+ gem "mime-types", "~> 1.0"
5
7
  gem "trollop", "~> 2.0"
6
8
 
7
9
  # Gems used to test and develop RightPublish
data/Gemfile.lock CHANGED
@@ -101,9 +101,11 @@ PLATFORMS
101
101
 
102
102
  DEPENDENCIES
103
103
  builder
104
+ excon (<= 0.25.3)
104
105
  flexmock (~> 0.9)
105
106
  fog (~> 1.9)
106
107
  jeweler (~> 1.8.3)
108
+ mime-types (~> 1.0)
107
109
  rake (~> 0.9)
108
110
  rdoc (>= 2.4.2)
109
111
  right_develop (~> 1.0)!
data/README.rdoc CHANGED
@@ -33,28 +33,28 @@ debian distros. It expects the "expect" package to be installed on all distros.
33
33
  # value of an attribute, or they can appear anywhere in
34
34
  # the attribute with static text surrounding them.
35
35
  #
36
- :verbose: # -: false
37
- :apt_repo:
38
- :dists: # *: [ woody, sid, etch, jaunty, lucid ]
39
- :auto: # -: true
40
- :subdir: # -: apt/
41
- :gpg_key_id: # *: 9A917D05
42
- :gpg_password: # *: @@GPG_PASSWORD@@
43
- :gem_repo:
44
- :subdir: # -: gems/
45
- :yum_repo:
46
- :dists: # *: el: ['6']
47
- :epel: # -: 1
48
- :subdir: # -: yum/
49
- :gpg_key_id: # *: 9A917D05
50
- :gpg_password: # *: @@GPG_PASSWORD@@
51
- :local_storage:
52
- :cache_dir # -: ~/.rp_cache
53
- :remote_storage:
54
- :storage_provider: # -: AWS
55
- :access_id: # *: @@AWS_ACCESS_KEY_ID@@
56
- :access_key: # *: @@AWS_SECRET_ACCESS_KEY@@
57
- :remote_path: # +: rightlink-testing
36
+ verbose: # -: false
37
+ apt_repo:
38
+ dists: # *: [ woody, sid, etch, jaunty, lucid ]
39
+ auto: # -: true
40
+ subdir: # -: apt/
41
+ gpg_key_id: # *: 9A917D05
42
+ gpg_password: # *: @@GPG_PASSWORD@@
43
+ gem_repo:
44
+ subdir: # -: gems/
45
+ yum_repo:
46
+ dists: # *: el: ['6']
47
+ epel: # -: 1
48
+ subdir: # -: yum/
49
+ gpg_key_id: # *: 9A917D05
50
+ gpg_password: # *: @@GPG_PASSWORD@@
51
+ local_storage:
52
+ cache_dir # -: ~/.right_publish/cache
53
+ remote_storage:
54
+ provider: # -: s3
55
+ access_id: # *: @@AWS_ACCESS_KEY_ID@@
56
+ access_key: # *: @@AWS_SECRET_ACCESS_KEY@@
57
+ remote_path: # +: rightlink-testing
58
58
  #
59
59
  # End of RightPublish Profile
60
60
  ###########################################################
@@ -69,12 +69,12 @@ debian distros. It expects the "expect" package to be installed on all distros.
69
69
  Usage:
70
70
  right_publish [global_options] <command> [cmd_options]
71
71
  commands:
72
- fetch
72
+ pull
73
73
  publish
74
- store
74
+ push
75
75
  global options:
76
76
  --profile, -p <s>: Publish profile
77
- --repo-type, -r <s>: Repository type: ["apt", "gem", "yum"]
77
+ --repo-type, -r <s>: Repository type: ["apt", "gem", "msi", "yum", "zypp"]
78
78
  --dist, -d: Target dist: ["el/6", "precise"]
79
79
  --verbose, -v: Verbose output
80
80
  --version, -e: Print version and exit
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.4.0
data/bin/right_publish CHANGED
@@ -1,8 +1,4 @@
1
1
  #! /usr/bin/env ruby
2
- #
3
- #
4
-
5
- $: << File.expand_path("../../lib",__FILE__)
6
2
 
7
3
  require 'right_publish'
8
- RightPublish::Main.run()
4
+ RightPublish::CLI.run()
@@ -0,0 +1,101 @@
1
+ require 'builder'
2
+
3
+ RightPublish::Profile.instance.register_section(:annotation,
4
+ :title => nil,
5
+ :scripts => [],
6
+ :stylesheets => [])
7
+
8
+ module RightPublish
9
+ module Annotation
10
+ module_function
11
+
12
+ # Generate an HTML index file for a set of packages.
13
+ # @param [Array] files list of relative file keys
14
+ # @param [Integer] strip number of path elements to strip from displayed filenames, default 0
15
+ # @option options [Array] :filter a whitelist of String glob patterns to filter files, i.e. ["*.rpm", "*.deb"]
16
+ # @option options [String] :title Human-readable page title
17
+ # @option options [Array] :scripts JavaScript hrefs to embed into page as <script> tags
18
+ # @option options [Array] :stylesheets Stylesheet hrefs to embed into page as <link> tags
19
+ def generate_html(files, strip=0, options={})
20
+ title = options[:title]
21
+ filter = options[:filter] || []
22
+ scripts = options[:scripts] || []
23
+ stylesheets = options[:stylesheets] || []
24
+
25
+ unless filter.nil? || filter.empty?
26
+ filtered = files.select { |f| filter.any? { |ff| File.fnmatch(ff, f.key) } }
27
+ end
28
+ segmented = filtered.map { |f| f.key.split('/') }
29
+
30
+ index = {}
31
+ segmented.each do |segments|
32
+ stripped = segments[strip..-1]
33
+
34
+ map = index
35
+ stripped.each_with_index do |seg, idx|
36
+ if idx == stripped.size - 1
37
+ map[seg] = "/#{segments.join('/')}"
38
+ else
39
+ map[seg] ||= {}
40
+ map = map[seg]
41
+ end
42
+ end
43
+ end
44
+
45
+ b = Builder::XmlMarkup.new(:indent => 2)
46
+
47
+ # Declare us as an HTML5 document
48
+ b.declare!(:DOCTYPE, :HTML)
49
+
50
+ b.html {
51
+ b.head {
52
+ # Ensure that browsers get a valid content type and charset, even if the Web server
53
+ # is misconfigured. This is necessary for our doc to be considered valid HTML5.
54
+ b.meta(:'http-equiv'=>'Content-Type', :content=>'text/html; charset=UTF-8')
55
+
56
+ if title
57
+ b.title(title)
58
+ end
59
+
60
+ stylesheets.each do |stylesheet|
61
+ b.link(:rel => 'stylesheet', :type => 'text/css', :href => stylesheet)
62
+ end
63
+
64
+ scripts.each do |script|
65
+ b.script(:src => script, :type => 'text/javascript')
66
+ end
67
+ }
68
+ b.body {
69
+ if title
70
+ b.h1(title, :class => 'title')
71
+ end
72
+ recursive_ul(index, b)
73
+ }
74
+ }
75
+ end
76
+
77
+ private
78
+
79
+ def self.recursive_ul(index, b)
80
+ b.ul(:class => 'dirList') {
81
+ index.keys.sort.each do |k|
82
+ v = index[k]
83
+
84
+ case v
85
+ when Hash
86
+ b.li(:class => 'dir') {
87
+ b.text! k
88
+ recursive_ul(v, b)
89
+ }
90
+ else
91
+ base = File.basename(v)
92
+ ext = File.extname(v)[1..-1]
93
+ b.li(:class => ext) {
94
+ b.a(base, :href => v)
95
+ }
96
+ end
97
+ end
98
+ }
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,91 @@
1
+ require 'trollop'
2
+
3
+ module RightPublish
4
+ module CLI
5
+ REPOSITORY_TYPES = RepoManager.repo_types()
6
+ SUB_COMMANDS = %w(publish pull add annotate push)
7
+
8
+ def self.run()
9
+ options = parse_args
10
+
11
+ begin
12
+ profile = RightPublish::Profile.instance
13
+ profile.load(options[:profile] || options[:repo_type])
14
+ rescue LoadError => e
15
+ puts e
16
+ exit -1
17
+ end
18
+
19
+ # These options override profile attributes.
20
+ profile.settings[:local_storage][:cache_dir] = options[:local_cache] if options[:local_cache]
21
+ profile.settings[:verbose] = true if options[:verbose]
22
+
23
+ # We already know this is a valid type, parse_args checked
24
+ repo = RepoManager.get_repository(REPOSITORY_TYPES[options[:repo_type]])
25
+ begin
26
+ case options[:cmd]
27
+ when 'publish'
28
+ repo.publish(options[:files], options[:dist])
29
+ when 'pull'
30
+ repo.pull
31
+ when 'add'
32
+ repo.add(options[:files], options[:dist])
33
+ when 'annotate'
34
+ repo.annotate
35
+ when 'push'
36
+ repo.push
37
+ else
38
+ raise ArgumentError, "Unknown command '#{options[:cmd]}'"
39
+ end
40
+ rescue RuntimeError => e
41
+ RightPublish::Profile.log("Fatal Error:\n\t#{e}", :error)
42
+ exit -1
43
+ end
44
+
45
+ exit 0
46
+ end
47
+
48
+ def self.parse_args()
49
+ options = Trollop.options do
50
+ version "RightPublish (c) 2013 RightScale Inc"
51
+ banner <<-EOS
52
+ RightPublish can manage a YUM/APT/RubyGem repository in remote storage, e.g. S3.
53
+
54
+ Usage:
55
+ right_publish [options] <command> [file1, file2, ...]
56
+
57
+ basic commands:
58
+ publish
59
+
60
+ advanced commands:
61
+ pull add annotate push
62
+
63
+ options:
64
+ EOS
65
+
66
+ opt :local_cache, "Local cache location", :type => String
67
+ opt :profile, "Publish profile", :type => String, :default => nil
68
+ opt :repo_type, "Repository type: #{REPOSITORY_TYPES.keys.inspect}", :type => String
69
+ opt :dist, "Target distribution. Required for binary packages. If unspecified for noarch and source packages, will copy to all distributions specified in profile.", :type => String
70
+ opt :verbose, "Verbose output"
71
+ end
72
+
73
+ options[:cmd]= ARGV.shift
74
+ options[:files] = expand_argv_globs
75
+
76
+ Trollop.die "invalid repository type: #{options[:repo_type]}" unless REPOSITORY_TYPES[options[:repo_type]]
77
+
78
+ options
79
+ end
80
+
81
+ def self.expand_argv_globs
82
+ files = []
83
+
84
+ ARGV.each do |glob|
85
+ files += Dir.glob(glob)
86
+ end
87
+
88
+ files
89
+ end
90
+ end
91
+ end
@@ -2,13 +2,10 @@ require 'singleton'
2
2
  require 'yaml'
3
3
 
4
4
  module RightPublish
5
-
6
5
  class Profile
7
-
8
- # Configuration defaults
9
- # =============
10
- DEFAULT_VERBOSE = false
11
- # =============
6
+ # Location where per-user profiles are stored
7
+ USER_PROFILE_DIR = File.expand_path('~/.right_publish/profiles')
8
+ DEFAULT_VERBOSE = false
12
9
 
13
10
  attr_accessor :settings
14
11
  include Singleton
@@ -21,22 +18,35 @@ module RightPublish
21
18
  puts(s) if Profile.config[:verbose] || level != :debug
22
19
  end
23
20
 
21
+ # Load a YML profile from disk. Profiles can be identified by their path on disk, or by the name
22
+ # of a YML file located in USER_PROFILE_DIR (without YML extension).
23
+ #
24
+ # @param [String] path absolute/relative path to file, or the name of per-user profile
25
+ # @return [true] always returns true
26
+ #
27
+ # @example per-user profile
28
+ # Profile.instance.load("gem")
29
+ #
30
+ # @example profile on disk
31
+ # Profile.instance.load("config/publish/nightly.yml")
32
+ #
33
+ # @see USER_PROFILE_DIR
24
34
  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}")
35
+ if File.exists?(path)
36
+ @settings ||= Profile.symbolize_profile(YAML.load_file(path))
37
+ validate_profile
38
+ elsif (path = File.join(USER_PROFILE_DIR, "#{path}.yml")) && File.exists?(path)
39
+ @settings ||= Profile.symbolize_profile(YAML.load_file(path))
40
+ validate_profile
41
+ else
42
+ raise LoadError, "Missing profile '#{path}'"
38
43
  end
39
- nil
44
+
45
+ true
46
+ rescue LoadError
47
+ raise
48
+ rescue Exception => e
49
+ raise LoadError, "Cannot load profile '#{path}' - #{e}"
40
50
  end
41
51
 
42
52
  def register_section(section_key, section_options)
@@ -5,7 +5,7 @@ module RightPublish
5
5
 
6
6
  class RepoManager
7
7
  @@repository_type_regex = /\A(\w+)_repo/
8
- @@repository_table = {}
8
+ @@repository_table = {}
9
9
 
10
10
  def self.get_repository(type)
11
11
  @@repository_table[type].new(type) if @@repository_table[type]
@@ -31,37 +31,85 @@ module RightPublish
31
31
  end
32
32
  end
33
33
 
34
- module Repo
34
+ class Repo
35
35
  def initialize(option_key)
36
36
  @repository_type ||= option_key
37
37
  end
38
38
 
39
- def fetch()
40
- Profile.log("Fetching latest #{repo_human_name(@repository_type)} data...")
39
+ # Sync files from remote to local storage
40
+ def pull()
41
+ Profile.log("Fetching #{repo_human_name(@repository_type)} files from remote...")
42
+
41
43
  begin
42
44
  sync_dirs(
43
45
  get_storage(Profile.config[:remote_storage][:provider]),
44
46
  get_storage(Profile.config[:local_storage][:provider]),
45
- Profile.config[@repository_type][:subdir] )
47
+ Profile.config[@repository_type][:subdir])
46
48
  rescue Exception => e
47
49
  RightPublish::Profile.log("Could not synchronize storage:\n\t#{e}", :error)
48
- raise RuntimeError, "fetch from remote failed."
50
+ raise RuntimeError, "pull from remote failed."
49
51
  end
50
52
  end
51
53
 
52
- def store()
53
- Profile.log("Commiting local #{repo_human_name(@repository_type)} data...")
54
+ # Sync files from local to remote storage
55
+ def push()
56
+ Profile.log("Committing local #{repo_human_name(@repository_type)} files to remote...")
57
+
54
58
  begin
55
59
  sync_dirs(
56
60
  get_storage(Profile.config[:local_storage][:provider]),
57
61
  get_storage(Profile.config[:remote_storage][:provider]),
58
- Profile.config[@repository_type][:subdir] )
62
+ Profile.config[@repository_type][:subdir])
59
63
  rescue Exception => e
60
64
  RightPublish::Profile.log("Could not sychronize storage:\n\t#{e}", :error)
61
- raise RuntimeError, "store to remote failed."
65
+ raise RuntimeError, "push to remote failed."
62
66
  end
63
67
  end
64
-
68
+
69
+ # Create an index.html file to enable browsing of this repo's subdir.
70
+ # @option options [String] :subdir a subdirectory (common prefix); only files matching this prefix will be included
71
+ # @option options [Array] :filter a whitelist of String glob patterns to filter files, i.e. ["*.rpm", "*.deb"]
72
+ def annotate(options={})
73
+
74
+ options[:subdir] ||= repo_config[:subdir]
75
+ storage_type = options[:use_remote_storage] ? :remote_storage : :local_storage
76
+
77
+ Profile.log("Creating HTML directory listing from #{storage_type.to_s} storage for #{repo_human_name(@repository_type)} files...")
78
+
79
+ files = []
80
+ RightPublish::Storage.ls(
81
+ get_storage(Profile.config[storage_type][:provider]),
82
+ :subdir => options[:subdir]) do |file|
83
+ files << file
84
+ end
85
+
86
+ # Build merged options hash for annotation, based on profile config plus some options
87
+ # passed into our class.
88
+ html_options = Profile.config[:annotation] ? Profile.config[:annotation].dup : {}
89
+ html_options[:filter] = options[:filter] if options.key?(:filter)
90
+
91
+ strip = options[:subdir].split('/').size
92
+ html = RightPublish::Annotation.generate_html(files, strip, html_options)
93
+
94
+ output = File.join(repo_config[:subdir], 'index.html')
95
+ local_dir = get_storage(Profile.config[:local_storage][:provider]).get_directories
96
+ local_dir.files.create(:key => output, :body => html)
97
+ end
98
+
99
+ # Perform one-shot publish of one or more packages.
100
+ # "One-shot" means: pull, add, annotate and push.
101
+ def publish(file_or_dir, target)
102
+ pull
103
+ add(file_or_dir, target)
104
+ annotate
105
+ push
106
+ end
107
+
108
+ # Add a new package to local storage and reindex if necessary
109
+ def add(file_or_dir, targe)
110
+ raise NotImplementedError, "Subclass responsibility"
111
+ end
112
+
65
113
  private
66
114
 
67
115
  def build_glob(ext)
@@ -77,16 +125,16 @@ module RightPublish
77
125
  end
78
126
 
79
127
  def get_pkg_list(file_or_dir, ext=nil)
80
- pkg_list = []
128
+ pkg_list = []
81
129
  file_or_dir = Array(file_or_dir)
82
130
  file_or_dir.each do |path|
83
131
  path = path.gsub("\\", "/")
84
132
  pkg_list << if File.directory?(path)
85
- glob_filter = (ext && "*.#{build_glob(ext)}") || "*"
86
- Dir.glob(File.join(path, glob_filter))
87
- else
88
- path.split(',')
89
- end
133
+ glob_filter = (ext && "*.#{build_glob(ext)}") || "*"
134
+ Dir.glob(File.join(path, glob_filter))
135
+ else
136
+ path.split(',')
137
+ end
90
138
  end
91
139
  pkg_list.flatten!
92
140
 
@@ -100,8 +148,8 @@ module RightPublish
100
148
  pkg_list
101
149
  end
102
150
 
103
- # For automation, we want to send the password, however gpg uses getpass
104
- # c function, which interacts directly with /dev/pty instead of stdin/stdout
151
+ # For automation, we want to send the password, however gpg uses getpass
152
+ # c function, which interacts directly with /dev/pty instead of stdin/stdout
105
153
  # to hide the password as its typed in. So, we need to allocate a pty.
106
154
  # We do this by shelling out to expect script (tcl based). Ruby 1.8
107
155
  # implementation of "expect" is broken and has some race conditions with
@@ -109,9 +157,9 @@ module RightPublish
109
157
  def shellout_with_password(cmd)
110
158
  password = repo_config[:gpg_password]
111
159
  raise Exception, ":gpg_password must be supplied when signing packages" unless password
112
- ENV['GPG_PASSWORD'] = password
113
- bin_dir = File.expand_path("../../../bin", __FILE__)
114
- autosign = File.join(bin_dir, "autosign.expect")
160
+ ENV['GPG_PASSWORD'] = password
161
+ bin_dir = File.expand_path("../../../bin", __FILE__)
162
+ autosign = File.join(bin_dir, "autosign.expect")
115
163
  system("#{autosign} #{cmd}")
116
164
  end
117
165
 
@@ -123,7 +171,7 @@ module RightPublish
123
171
  def install_file(file, dest)
124
172
  Profile.log("#{file} => #{dest}")
125
173
  local_dir = get_storage(Profile.config[:local_storage][:provider]).get_directories
126
- File.open(file, "rb") { |chunk| local_dir.files.create(:key=>File.join(dest, File.basename(file)), :body=>chunk) }
174
+ File.open(file, "rb") { |chunk| local_dir.files.create(:key => File.join(dest, File.basename(file)), :body => chunk) }
127
175
  end
128
176
 
129
177
  def prune_all(glob_pattern)
@@ -136,16 +184,17 @@ module RightPublish
136
184
  end
137
185
 
138
186
  def repo_human_name(type)
139
- type.to_s.sub(/_/,' ')
187
+ type.to_s.sub(/_/, ' ')
140
188
  end
141
189
 
142
190
  def sync_dirs(src, dest, subdir='')
143
- RightPublish::Storage.sync_dirs(src, dest, :subdir=>subdir, :sweep=>true)
191
+ RightPublish::Storage.sync_dirs(src, dest, :subdir => subdir, :sweep => true)
144
192
  end
145
193
  end
146
194
  end
147
195
 
148
196
  require 'right_publish/repos/apt'
149
197
  require 'right_publish/repos/gem'
198
+ require 'right_publish/repos/msi'
150
199
  require 'right_publish/repos/yum'
151
200
  require 'right_publish/repos/zypp'