puppet-armature 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.
@@ -0,0 +1,7 @@
1
+ module Armature
2
+ class Error < StandardError
3
+ end
4
+
5
+ class RefError < Armature::Error
6
+ end
7
+ end
@@ -2,26 +2,52 @@ module Armature
2
2
  class Puppetfile
3
3
  attr_reader :results
4
4
 
5
- def initialize()
5
+ def initialize(cache)
6
+ @cache = cache
6
7
  @results = {}
8
+ @logger = Logging.logger[self]
9
+ @forge_url = "https://forge.puppet.com"
7
10
  end
8
11
 
12
+ ### FIXME this will have access to @cache and @results
9
13
  def include(path)
10
14
  instance_eval(IO.read(path), path)
11
15
  @results
12
16
  end
13
17
 
14
- def mod(name, options={})
15
- if name =~ /\A\./
16
- raise "Module name may not start with period: '#{name}'"
17
- elsif name =~ /\//
18
- raise "Module name may not contain /: '#{name}'"
19
- end
18
+ def forge(url)
19
+ @forge_url = url.chomp("/")
20
+ end
21
+
22
+ def mod(full_name, options={})
23
+ name = full_name.split("-", 2).last()
24
+ Armature::Environments.assert_valid_module_name(name)
20
25
 
21
26
  if @results[name]
22
27
  raise "Module #{name} declared twice"
23
28
  end
24
29
 
30
+ if options.is_a?(String) || options == {} || options == nil
31
+ _mod_forge(full_name, name, options)
32
+ elsif options[:git]
33
+ _mod_git(name, options)
34
+ else
35
+ raise "Invalid mod call: #{full_name} #{options}"
36
+ end
37
+ end
38
+
39
+ def _mod_forge(full_name, name, version="latest")
40
+ if version == nil || version == {}
41
+ version = "latest"
42
+ end
43
+
44
+ repo = Repo::Forge.from_url(@cache, @forge_url, full_name)
45
+ ref = repo.general_ref(version)
46
+ @logger.debug("mod #{name}: #{ref}")
47
+ @results[name] = { :name => name, :ref => ref }
48
+ end
49
+
50
+ def _mod_git(name, options={})
25
51
  options = Armature::Util.process_options(options, {
26
52
  :commit => nil,
27
53
  :tag => nil,
@@ -31,45 +57,43 @@ module Armature
31
57
  :git => nil,
32
58
  })
33
59
 
60
+ repo = Repo::Git.from_url(@cache, options[:git])
34
61
  ref = nil
35
62
 
36
63
  if options[:commit]
37
64
  if ref
38
65
  raise "Module #{name} has more than one of :commit, :tag, :branch, or :ref"
39
66
  end
40
- ref = options[:commit]
67
+ ref = repo.identity_ref(options[:commit])
41
68
  end
42
69
 
43
70
  if options[:tag]
44
71
  if ref
45
72
  raise "Module #{name} has more than one of :commit, :tag, :branch, or :ref"
46
73
  end
47
- ref = "refs/tags/#{options[:tag]}"
74
+ ref = repo.tag_ref(options[:tag])
48
75
  end
49
76
 
50
77
  if options[:branch]
51
78
  if ref
52
79
  raise "Module #{name} has more than one of :commit, :tag, :branch, or :ref"
53
80
  end
54
- ref = "refs/heads/#{options[:branch]}"
81
+ ref = repo.branch_ref(options[:branch])
55
82
  end
56
83
 
57
84
  if options[:ref]
58
85
  if ref
59
86
  raise "Module #{name} has more than one of :commit, :tag, :branch, or :ref"
60
87
  end
61
- ref = options[:ref]
88
+ ref = repo.general_ref(options[:ref])
62
89
  end
63
90
 
64
91
  if ! ref
65
- ref = "refs/heads/master"
92
+ ref = repo.branch_ref("master")
66
93
  end
67
94
 
68
- @results[name] = { :name => name, :ref => ref, :git => options[:git] }
69
- end
70
-
71
- def forge(*arguments)
72
- ### error?
95
+ @logger.debug("mod #{name}: #{ref}")
96
+ @results[name] = { :name => name, :ref => ref }
73
97
  end
74
98
  end
75
99
  end
@@ -0,0 +1,21 @@
1
+ module Armature::Ref
2
+ class Base
3
+ attr_reader :repo, :canonical_name, :identity, :human_type, :human_name
4
+
5
+ def initialize(repo, canonical_name, identity, human_type, human_name)
6
+ @repo = repo
7
+ @canonical_name = canonical_name
8
+ @identity = identity
9
+ @human_type = human_type
10
+ @human_name = human_name
11
+ end
12
+
13
+ def check_out
14
+ @repo.check_out(self)
15
+ end
16
+
17
+ def to_s
18
+ "#{@human_type} \"#{@human_name}\""
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ class Armature::Ref::Identity < Armature::Ref::Base
2
+ def type
3
+ :identity
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Armature::Ref::Immutable < Armature::Ref::Base
2
+ def type
3
+ :immutable
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Armature::Ref::Mutable < Armature::Ref::Base
2
+ def type
3
+ :mutable
4
+ end
5
+ end
@@ -0,0 +1,56 @@
1
+ class Armature::Repo
2
+ def self.from_path(cache, path)
3
+ # Called from the cache; don't check for an existing repo
4
+ self.new(cache, path)
5
+ end
6
+
7
+ def initialize(cache, repo_dir)
8
+ @cache = cache
9
+ @repo_dir = repo_dir
10
+ @logger = Logging.logger[self]
11
+ @url = nil
12
+ flush_memory!
13
+
14
+ @cache.register_repo(self)
15
+ end
16
+
17
+ # You may wish to override these methods
18
+
19
+ def url
20
+ @url
21
+ end
22
+
23
+ def check_out(ref)
24
+ end
25
+
26
+ def freshen!
27
+ flush_memory!
28
+ end
29
+
30
+ def flush_memory!
31
+ @fresh = false
32
+ end
33
+
34
+ # Generally these don't need to be overridden
35
+
36
+ def self.type
37
+ self.name.split('::').last().downcase()
38
+ end
39
+
40
+ def type
41
+ self.class.type()
42
+ end
43
+
44
+ def to_s
45
+ url()
46
+ end
47
+
48
+ def freshen
49
+ if ! @fresh
50
+ freshen!
51
+ true
52
+ else
53
+ false
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,154 @@
1
+ # Get a module from the Forge
2
+ class Armature::Repo::Forge < Armature::Repo
3
+ CANONICAL_FORGE_URL = "https://forge.puppet.com"
4
+ FORGE_URLS = [
5
+ "https://forge.puppetlabs.com",
6
+ "https://forgeapi.puppetlabs.com",
7
+ "https://forgeapi.puppet.com",
8
+ "http://forge.puppetlabs.com",
9
+ "http://forgeapi.puppetlabs.com",
10
+ "http://forge.puppet.com",
11
+ "http://forgeapi.puppet.com",
12
+ ]
13
+
14
+ def self.normalize_forge_url(url)
15
+ url.chomp!("/")
16
+ if FORGE_URLS.include? url
17
+ CANONICAL_FORGE_URL
18
+ else
19
+ url
20
+ end
21
+ end
22
+
23
+ # Gets a repo object for a given Forge module. This just contains metadata.
24
+ def self.from_url(cache, forge_url, full_name)
25
+ forge_url = self.normalize_forge_url(forge_url)
26
+
27
+ url = "#{forge_url}/#{full_name}"
28
+ repo = cache.get_repo("forge", url)
29
+ if repo
30
+ return repo
31
+ end
32
+
33
+ repo_dir = cache.open_repo("forge", url) do |temp_path|
34
+ File.write("#{temp_path}/url", "#{url}\n")
35
+ File.write("#{temp_path}/forge_url", "#{forge_url}\n")
36
+ File.write("#{temp_path}/full_name", "#{full_name}\n")
37
+
38
+ Logging.logger[self].debug("Created stub repo for '#{url}'")
39
+ end
40
+
41
+ return self.new(cache, repo_dir)
42
+ end
43
+
44
+ def url
45
+ @url || File.read("#{@repo_dir}/url").chomp()
46
+ end
47
+
48
+ def forge_url
49
+ @forge_url || File.read("#{@repo_dir}/forge_url").chomp()
50
+ end
51
+
52
+ def full_name
53
+ @full_name || File.read("#{@repo_dir}/full_name").chomp()
54
+ end
55
+
56
+ # Ensure we've got the correct version and return its path
57
+ def check_out(ref)
58
+ @cache.open_ref(ref) do |object_path|
59
+ @cache.open_temp() do
60
+ @logger.debug("Downloading #{ref} from #{url}")
61
+ url = forge_url() + release_metadata(ref.identity, "file_uri")
62
+
63
+ tarball = Armature::Util::http_get(url)
64
+
65
+ # This extracts into a temp directory
66
+ Armature::Run::pipe_command(tarball, "tar", "xzf" , "-")
67
+
68
+ # Move everything into the object directory
69
+ extract_dir = Dir["*"].first()
70
+ Dir.entries(extract_dir).each do |child|
71
+ if ! [".", ".."].include?(child)
72
+ File.rename("#{extract_dir}/#{child}", "#{object_path}/#{child}")
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def freshen!
80
+ flush_memory!
81
+ download_metadata()
82
+ end
83
+
84
+ def flush_memory!
85
+ @fresh = false
86
+ @metadata = nil
87
+ end
88
+
89
+ # Get ref object
90
+ def general_ref(version)
91
+ if version.nil? || version == "latest"
92
+ latest_ref()
93
+ else
94
+ version_ref(version)
95
+ end
96
+ end
97
+
98
+ def version_ref(version)
99
+ Armature::Ref::Immutable.new(self, version, version, "version", version)
100
+ end
101
+
102
+ def latest_ref
103
+ freshen()
104
+ version = metadata("current_release.version")
105
+
106
+ Armature::Ref::Mutable.new(self, "latest", version, "version", "latest")
107
+ end
108
+
109
+ private
110
+
111
+ def metadata(path=nil)
112
+ ### FIXME cache metadata in repo
113
+ @metadata ||= download_metadata()
114
+ return @metadata if path == nil
115
+
116
+ begin
117
+ dig(@metadata, path.split("."))
118
+ rescue => e
119
+ raise "Got invalid metadata from #{metadata_url()}: #{e}"
120
+ end
121
+ end
122
+
123
+ def release_metadata(version, path=nil)
124
+ metadata("releases").each do |release|
125
+ begin
126
+ if release["version"] == version
127
+ return dig(release, path.split("."))
128
+ end
129
+ rescue => e
130
+ raise "Got invalid metadata from #{metadata_url()}: #{e}"
131
+ end
132
+ end
133
+
134
+ raise "Release #{version} not found"
135
+ end
136
+
137
+ def metadata_url
138
+ [forge_url(), "v3", "modules", full_name()].join("/")
139
+ end
140
+
141
+ def download_metadata
142
+ @fresh = true
143
+ @metadata = Armature::Util::http_get_json(metadata_url())
144
+ end
145
+
146
+ def dig(parent, path)
147
+ key = path.shift()
148
+ if path.length() == 0
149
+ parent[key]
150
+ else
151
+ dig(parent[key], path)
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,228 @@
1
+ class Armature::Repo::Git < Armature::Repo
2
+ # Gets a repo object for a given URL. Mirrors the repo in the local cache
3
+ # if it doesn't already exist.
4
+ def self.from_url(cache, url)
5
+ repo = cache.get_repo("git", url)
6
+ if repo
7
+ return repo
8
+ end
9
+
10
+ fresh = false
11
+ repo_dir = cache.open_repo("git", url) do |temp_path|
12
+ Logging.logger[self].debug("Cloning '#{url}' for the first time")
13
+
14
+ # Mirror copies *all* refs, not just branches. Ignore output.
15
+ Armature::Run.clean_git("clone", "--quiet", "--mirror", url, temp_path)
16
+ fresh = true
17
+
18
+ Logging.logger[self].debug("Done cloning '#{url}'")
19
+ end
20
+
21
+ return self.new(cache, repo_dir, fresh)
22
+ end
23
+
24
+ def initialize(cache, repo_dir, is_fresh=false)
25
+ super(cache, repo_dir)
26
+ @fresh = is_fresh
27
+ end
28
+
29
+ def url
30
+ @url ||= git("config", "--get", "remote.origin.url").chomp()
31
+ end
32
+
33
+ # Check out a ref from a repo and return the path
34
+ def check_out(ref)
35
+ @cache.open_ref(ref) do |object_path|
36
+ git "reset", "--hard", ref.identity, :work_dir=>object_path
37
+ end
38
+ end
39
+
40
+ def freshen!
41
+ flush_memory!
42
+
43
+ @logger.info("Fetching from #{url}")
44
+ Armature::Util::lock(@repo_dir, File::LOCK_EX, "fetch") do
45
+ ### FIXME Only flush memory if this makes changes
46
+ git "remote", "update", "--prune"
47
+ end
48
+ @fresh = true
49
+ end
50
+
51
+ def flush_memory!
52
+ @fresh = false
53
+ @ref_cache = {}
54
+ @rev_cache = {}
55
+ end
56
+
57
+ # Get ref object
58
+ def general_ref(ref_str)
59
+ return @ref_cache[ref_str] if @ref_cache[ref_str]
60
+
61
+ if ref_str.start_with? "refs/heads/"
62
+ return branch_ref(ref_str.sub("refs/heads/", ""))
63
+ end
64
+
65
+ if ref_str.start_with? "refs/tags/"
66
+ return tag_ref(ref_str.sub("refs/tags/", ""))
67
+ end
68
+
69
+ retry_fresh do
70
+ if ref_str.start_with? "refs/"
71
+ freshen()
72
+ return make_ref(Armature::Ref::Mutable, ref_str, rev_parse!(ref_str),
73
+ "ref", ref_str)
74
+ end
75
+
76
+ if sha = rev_parse("refs/heads/#{ref_str}")
77
+ return branch_ref(ref_str)
78
+ end
79
+
80
+ if sha = rev_parse("refs/tags/#{ref_str}")
81
+ return tag_ref(ref_str)
82
+ end
83
+
84
+ # This could trigger a retry
85
+ sha = rev_parse!(ref_str)
86
+ if sha_match? sha, ref_str
87
+ return identity_ref(sha)
88
+ end
89
+
90
+ # It exists, but it's outside of refs/. Treat it as mutable.
91
+ make_ref(Armature::Ref::Mutable, ref_str, sha, "ref", ref_str)
92
+ end
93
+ end
94
+
95
+ # Get ref object for some sort of mutable ref we found in the FS cache
96
+ def mutable_fs_ref(ref_str)
97
+ freshen()
98
+
99
+ return @ref_cache[ref_str] if @ref_cache[ref_str]
100
+
101
+ if ref_str.start_with? "refs/heads/"
102
+ return branch_ref(ref_str.sub("refs/heads/", ""))
103
+ end
104
+
105
+ if ref_str.start_with? "refs/tags/"
106
+ return tag_ref(ref_str.sub("refs/tags/", ""))
107
+ end
108
+
109
+ make_ref(Armature::Ref::Mutable, ref_str, rev_parse!(ref_str), "ref", ref_str)
110
+ end
111
+
112
+ # The identity of an object itself, i.e. a SHA
113
+ def identity_ref(sha)
114
+ return @ref_cache[sha] if @ref_cache[sha]
115
+
116
+ real_sha = nil # needs to be bound to this scope
117
+ retry_fresh do
118
+ # real_sha will always be a full SHA, but sha might be partial
119
+ real_sha = rev_parse!(sha)
120
+ if ! sha_match? real_sha, sha
121
+ raise Armature::RefError, "'#{sha}' is not a Git SHA"
122
+ end
123
+ end
124
+
125
+ make_ref(Armature::Ref::Identity, real_sha, real_sha, "revision", real_sha)
126
+
127
+ # If sha is partial, register it in the cache too.
128
+ @ref_cache[sha] = @ref_cache[real_sha]
129
+ end
130
+
131
+ def branch_ref(name)
132
+ ref_str = "refs/heads/#{name}"
133
+ freshen()
134
+
135
+ return @ref_cache[ref_str] if @ref_cache[ref_str]
136
+
137
+ sha = rev_parse!(ref_str)
138
+ _branch_ref(ref_str, name, sha)
139
+ end
140
+
141
+ def _branch_ref(ref_str, name, sha)
142
+ make_ref(Armature::Ref::Mutable, ref_str, sha, "branch", name)
143
+ @ref_cache[name] = @ref_cache[ref_str]
144
+ end
145
+
146
+ def tag_ref(name)
147
+ ref_str = "refs/tags/#{name}"
148
+ return @ref_cache[ref_str] if @ref_cache[ref_str]
149
+
150
+ sha = retry_fresh { rev_parse!(ref_str) }
151
+ make_ref(Armature::Ref::Immutable, ref_str, sha, "tag", name)
152
+ @ref_cache[name] = @ref_cache[ref_str]
153
+ end
154
+
155
+ def git(*arguments)
156
+ # This accepts a hash of options as the last argument
157
+ options = if arguments.last.is_a? Hash then arguments.pop else {} end
158
+ options = Armature::Util.process_options(options, { :work_dir => nil }, {})
159
+
160
+ if options[:work_dir]
161
+ work_dir_arguments = [ "--work-tree=" + options[:work_dir] ]
162
+ else
163
+ work_dir_arguments = []
164
+ end
165
+
166
+ command = [ "--git-dir=#{@repo_dir}" ] \
167
+ + work_dir_arguments \
168
+ + arguments
169
+
170
+ Armature::Run.clean_git(*command)
171
+ end
172
+
173
+ def get_branches()
174
+ freshen()
175
+ data = git("for-each-ref",
176
+ "--format", "%(objectname) %(refname)",
177
+ "refs/heads")
178
+ lines = data.split(/[\r\n]/).reject { |line| line == "" }
179
+
180
+ lines.map do |line|
181
+ sha, ref_str = line.split(' ', 2)
182
+ name = ref_str.sub("refs/heads/", "")
183
+ _branch_ref(ref_str, name, sha)
184
+ name
185
+ end
186
+ end
187
+
188
+ private
189
+
190
+ def retry_fresh
191
+ yield
192
+ rescue Armature::RefError
193
+ if @fresh
194
+ raise
195
+ end
196
+
197
+ @logger.debug("Got ref error; fetching to see if that helps.")
198
+ freshen()
199
+ yield
200
+ end
201
+
202
+ def make_ref(klass, ref_str, sha, type, name)
203
+ @ref_cache[ref_str] = klass.new(self, ref_str, sha, type, name)
204
+ end
205
+
206
+ # Get the SHA for a ref, or nil if it doesn't exist
207
+ def rev_parse(ref_str)
208
+ rev_parse!(ref_str)
209
+ rescue Armature::RefError
210
+ nil
211
+ end
212
+
213
+ # Get the SHA for a ref, or raise if it doesn't exist
214
+ def rev_parse!(ref_str)
215
+ if @rev_cache[ref_str]
216
+ @rev_cache[ref_str]
217
+ end
218
+
219
+ @rev_cache[ref_str] = git("rev-parse", "--verify", "#{ref_str}^{commit}").chomp
220
+ rescue Armature::Run::CommandFailureError
221
+ raise Armature::RefError, "no such ref '#{ref_str}' in repo '#{self}'"
222
+ end
223
+
224
+ # Check if a real (full) SHA matches a (possibly partial) candidate SHA
225
+ def sha_match? real, candidate
226
+ real.start_with? candidate
227
+ end
228
+ end