right_publish 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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'