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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 20b1e802ab0134205a8fac195ba528a3664fd16f
4
- data.tar.gz: c1a48279c939c21b4ae482ced27e44243c69c4fc
3
+ metadata.gz: 39ee81e7821523113eb5b9868a0dca398ef3a6b0
4
+ data.tar.gz: 31da92d00fb15e345c8fdb28fce24e6e07fb1ec0
5
5
  SHA512:
6
- metadata.gz: ee52d4da30d62132d98b0e0246c4ce6aeb5c99126e7ce9538fe7de28e72c7926f6d33edc377d403bb048d9b2e917ad1f4685b61703b32bf2d2435f85f019b1dc
7
- data.tar.gz: 0da4f27f3df184cc48c1ae5d8383d7a62a874573a15fca64a3aff0cb54323144ef65da983a7c10c85d81c3977ce7e307abf1116ea5353fd8b7b32c28ce371a19
6
+ metadata.gz: a2c928a9daa31005a7d80b757080c6f41cd7b3785703c05bb61b10a51df848675b7907da9b63381d15a48b745ce6dfe3de1c19d50cbb7dbea2f83dfc69a8f141
7
+ data.tar.gz: d174e15de2dd26fbe228fed75383ffc6377afd166e79388a690e689f6d8f27329ebc4603e40bdd65e111cb91b94a2e295ce9f7b7fecf14d1f707bba29b87bba9
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- puppet-armature (0.2.2)
4
+ puppet-armature (0.4.0)
5
5
  gli (= 2.14.0)
6
6
  logging (~> 2)
7
7
 
@@ -9,19 +9,20 @@ GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
11
  gli (2.14.0)
12
- little-plugger (1.1.3)
13
- logging (2.0.0)
12
+ little-plugger (1.1.4)
13
+ logging (2.2.2)
14
14
  little-plugger (~> 1.1)
15
15
  multi_json (~> 1.10)
16
16
  minitest (5.10.1)
17
- multi_json (1.12.1)
17
+ multi_json (1.13.1)
18
18
 
19
19
  PLATFORMS
20
20
  ruby
21
+ x86_64-darwin-16
21
22
 
22
23
  DEPENDENCIES
23
24
  minitest (~> 5.9)
24
25
  puppet-armature!
25
26
 
26
27
  BUNDLED WITH
27
- 1.14.6
28
+ 1.17.1
data/TODO.md CHANGED
@@ -1,13 +1,5 @@
1
1
  # Future development
2
2
 
3
- ### Update module branches when updating an environment
4
-
5
- Rather than waiting for a periodic `update-branches` job to run, update module
6
- branches when their environment is deployed.
7
-
8
- This will require some caching so that work is not duplicated when updating all
9
- environments.
10
-
11
3
  ### Webhook endpoints
12
4
 
13
5
  I'm unsure if I want this. Webhooks are definitely useful, but I can get that
data/bin/armature CHANGED
@@ -20,6 +20,9 @@ switch [:v,:verbose]
20
20
  desc 'Show DEBUG level output'
21
21
  switch [:d,:debug]
22
22
 
23
+ desc 'Add timestamp for each line in log'
24
+ switch [:"log-time"]
25
+
23
26
  if Process.uid == 0
24
27
  environments_default = '/etc/puppetlabs/code/environments'
25
28
  cache_default = '/srv/armature-cache'
@@ -47,12 +50,12 @@ command "deploy-ref" do |c|
47
50
  # This should fail as early as possible (e.g. if the path isn't correct)
48
51
  environments = Armature::Environments.new(@environments_path, @cache)
49
52
 
50
- repo = @cache.get_repo(arguments.shift)
53
+ repo = Armature::Repo::Git.from_url(@cache, arguments.shift)
51
54
  ref = arguments.shift
52
55
  name = arguments.shift
53
56
 
54
57
  begin
55
- environments.checkout_ref(repo, ref, name)
58
+ environments.check_out_ref(repo, ref, name)
56
59
  rescue
57
60
  @logger.error("Error deploying ref '#{ref}' as '#{name}'")
58
61
  raise
@@ -68,11 +71,14 @@ arg 'BRANCH', :multiple=>true
68
71
  command "deploy-branches" do |c|
69
72
  c.desc "Delete environments that aren't being deployed"
70
73
  c.switch [:d,"delete-other"]
74
+ c.desc "Fix environment names instead of skipping them"
75
+ c.switch [:x,"fix-environment-names"]
71
76
  c.action do |global_options, options, arguments|
72
77
  # This should fail as early as possible (e.g. if the path isn't correct)
73
78
  environments = Armature::Environments.new(@environments_path, @cache)
79
+ environments.fix_environment_names = options["fix-environment-names"]
74
80
 
75
- repo = @cache.get_repo(arguments.shift)
81
+ repo = Armature::Repo::Git.from_url(@cache, arguments.shift)
76
82
  all_branches = repo.get_branches()
77
83
  branches = Set.new()
78
84
 
@@ -96,7 +102,7 @@ command "deploy-branches" do |c|
96
102
 
97
103
  branches.each do |branch|
98
104
  begin
99
- environments.checkout_ref(repo, branch)
105
+ environments.check_out_ref(repo, branch)
100
106
  rescue => e
101
107
  @logger.error("Error deploying branch '#{branch}' (skipping): #{e}")
102
108
  end
@@ -111,11 +117,20 @@ end
111
117
 
112
118
  desc 'Update all branches in the cache'
113
119
  command :update do |c|
114
- c.action { @cache.update_branches() }
120
+ c.action { @cache.update_mutable_refs() }
115
121
  end
116
122
 
117
123
  pre do |global_options, command, options, arguments|
118
- Logging.logger.root.add_appenders Logging.appenders.stdout
124
+ appender = Logging.appenders.stdout
125
+
126
+ if global_options[:"log-time"]
127
+ appender.layout = Logging.layouts.pattern.new(
128
+ :pattern => "%d %-5l [%c] %m\n",
129
+ :date_pattern => "%Y-%m-%d %H:%M:%S",
130
+ )
131
+ end
132
+
133
+ Logging.logger.root.add_appenders(appender)
119
134
 
120
135
  Logging.logger.root.level = :info if global_options[:verbose]
121
136
  Logging.logger.root.level = :debug if global_options[:debug]
data/lib/armature.rb CHANGED
@@ -1,10 +1,18 @@
1
+ module Armature
2
+ end
3
+
4
+ require "armature/errors.rb"
5
+
1
6
  require "armature/cache.rb"
2
7
  require "armature/environments.rb"
3
- require "armature/gitrepo.rb"
4
8
  require "armature/puppetfile.rb"
9
+ require "armature/ref/base.rb"
10
+ require "armature/ref/identity.rb"
11
+ require "armature/ref/immutable.rb"
12
+ require "armature/ref/mutable.rb"
13
+ require "armature/repo.rb"
14
+ require "armature/repo/git.rb"
15
+ require "armature/repo/forge.rb"
5
16
  require "armature/run.rb"
6
17
  require "armature/util.rb"
7
18
  require "armature/version.rb"
8
-
9
- module Armature
10
- end
@@ -19,73 +19,48 @@ module Armature
19
19
  end
20
20
  end
21
21
 
22
- def flush_memory!
23
- @logger.debug("Flushing in-memory caches for all repos")
24
- @repos.each do |name, repo|
25
- repo.freshen!
22
+ # Get the path to a repo on disk.
23
+ #
24
+ # If the repo doesn't exist locally, then it creates the path, locks it,
25
+ # and runs the passed block to create the repo.
26
+ def open_repo(type, url)
27
+ repo_id = _fs_repo_id(type, url)
28
+ repo_path = "#{@path}/repo/#{repo_id}"
29
+
30
+ if Dir.exist? repo_path
31
+ return repo_path
26
32
  end
27
- end
28
33
 
29
- # Get GitRepo object for a local clone of a remote repo at a URL
30
- #
31
- # This will clone the repo if it doesn't already exist.
32
- def get_repo(url)
33
- safe_url = fs_sanitize(url)
34
-
35
- repo_path = "#{@path}/repo/#{safe_url}"
36
- if ! Dir.exist? repo_path
37
- @logger.info("Cloning '#{url}' for the first time")
38
- Armature::Util::lock repo_path, File::LOCK_EX, "clone" do
39
- if Dir.exist? repo_path
40
- @logger.info("Another process cloned '#{url}' while we were blocked")
41
- else
42
- # Mirror copies *all* refs, not just branches. Ignore output.
43
- Armature::Run.command(
44
- "git", "clone", "--quiet", "--mirror", url, repo_path)
45
- @logger.debug("Done cloning '#{url}'")
46
- end
34
+ @logger.debug("Creating repo for #{type} '#{url}'")
35
+ Armature::Util::lock(repo_path, File::LOCK_EX, "create") do
36
+ if Dir.exist? repo_path
37
+ @logger.debug("Another process created repo for #{type} '#{url}' while we were waiting")
38
+ return repo_path
47
39
  end
48
- end
49
40
 
50
- get_repo_by_name(safe_url)
51
- end
41
+ temp_path = new_temp_path()
42
+ Dir.mkdir(temp_path)
52
43
 
53
- # Get a GitRepo object for an existing local repo by its santized URL
54
- def get_repo_by_name(safe_url)
55
- @repos[safe_url] ||= GitRepo.new("#{@path}/repo/#{safe_url}", safe_url)
56
- end
44
+ yield temp_path
57
45
 
58
- # Check out a ref from a repo and return the path
59
- def checkout(repo, ref, refresh=false, options={})
60
- if refresh
61
- # Don't check the cache; refresh it from source.
62
- repo.freshen()
63
- ref = repo.canonical_ref(ref)
64
- else
65
- # This will raise a RefError if the ref doesn't exist
66
- begin
67
- ref = repo.canonical_ref(ref)
68
- rescue RefError
69
- repo.freshen()
70
- ref = repo.canonical_ref(ref)
71
- end
46
+ File.rename(temp_path, repo_path)
72
47
  end
73
48
 
74
- type = repo.ref_type(ref)
75
- safe_ref = fs_sanitize(ref)
76
- repo_path = "#{@path}/ref/#{type}/#{repo.name}"
77
- ref_path = "#{repo_path}/#{safe_ref}"
49
+ return repo_path
50
+ end
78
51
 
79
- if ! refresh
80
- # Check cache first
81
- if Dir.exist? ref_path
82
- return ref_path
83
- end
84
- end
52
+ def open_ref(ref)
53
+ safe_ref = fs_sanitize(ref.canonical_name)
54
+ repo_path = "#{@path}/ref/#{ref.type}/#{fs_repo_id(ref.repo)}"
55
+ ref_path = "#{repo_path}/#{safe_ref}"
85
56
 
86
57
  FileUtils.mkdir_p(repo_path)
87
58
 
88
- identity_path = checkout_identity(repo, repo.ref_identity(ref))
59
+ @logger.debug("Checking for #{ref} in #{ref.repo}")
60
+ identity_path = open_identity(ref.repo, ref.identity) do |object_path|
61
+ yield object_path
62
+ end
63
+
89
64
  if identity_path != ref_path
90
65
  atomic_symlink(identity_path, ref_path)
91
66
  end
@@ -93,6 +68,33 @@ module Armature
93
68
  ref_path
94
69
  end
95
70
 
71
+ def register_repo(repo)
72
+ @repos[fs_repo_id(repo)] = repo
73
+ end
74
+
75
+ # Takes a string like "git:https://github.com/a/b.git" and returns Repo::Git
76
+ def repo_klass(repo_id)
77
+ type = repo_id.split(":", 2).first
78
+ Repo.const_get(type.capitalize())
79
+ end
80
+
81
+ # Get a repo object for an existing local repo by its santized URL
82
+ def repo_by_id(repo_id)
83
+ @repos[repo_id] ||= repo_klass(repo_id).from_path(self, "#{@path}/repo/#{repo_id}")
84
+ end
85
+
86
+ def get_repo(type, url)
87
+ @repos[_fs_repo_id(type, url)]
88
+ end
89
+
90
+ # This is used in the testing code
91
+ def flush_memory!
92
+ @logger.debug("Flushing in-memory caches for all repos")
93
+ @repos.each do |name, repo|
94
+ repo.flush_memory!
95
+ end
96
+ end
97
+
96
98
  # Creates a symlink atomically
97
99
  #
98
100
  # That is, if a symlink or file exists at new_path, then this will replace
@@ -102,8 +104,6 @@ module Armature
102
104
  def atomic_symlink(target_path, new_path)
103
105
  new_path.chomp!("/")
104
106
 
105
- @logger.debug("#{new_path} -> #{target_path}")
106
-
107
107
  begin
108
108
  if File.readlink(new_path) == target_path
109
109
  return
@@ -111,6 +111,8 @@ module Armature
111
111
  rescue Errno::ENOENT
112
112
  end
113
113
 
114
+ @logger.debug("Updating symlink #{new_path} -> #{target_path}")
115
+
114
116
  temp_path = new_temp_path()
115
117
  File.symlink(target_path, temp_path)
116
118
  File.rename(temp_path, new_path)
@@ -119,15 +121,15 @@ module Armature
119
121
  raise
120
122
  end
121
123
 
122
- def update_branches()
124
+ def update_mutable_refs()
123
125
  Dir.glob("#{@path}/ref/mutable/*/*") do |path|
124
- ref = fs_unsanitize(File.basename(path))
125
- repo = get_repo_by_name(File.basename(File.dirname(path)))
126
- @logger.info("Updating #{ref} ref from #{repo.url}")
126
+ repo = repo_by_id(File.basename(File.dirname(path)))
127
+ ref = repo.mutable_fs_ref(fs_unsanitize(File.basename(path)))
128
+ @logger.info("Updating #{ref} ref from #{repo}")
127
129
 
128
130
  begin
129
- checkout(repo, ref, :refresh=>true)
130
- rescue RefError
131
+ ref.check_out()
132
+ rescue Armature::RefError
131
133
  # The ref no longer exists, so we can't update it. Leave the old
132
134
  # checkout in place for safety; garbage collection will remove it if
133
135
  # it's no longer used.
@@ -167,6 +169,18 @@ module Armature
167
169
  @lock_file = nil
168
170
  end
169
171
 
172
+ # Open a temp directory, chdir into it, yield, then delete the directory
173
+ def open_temp
174
+ path = new_temp_path()
175
+
176
+ Dir.mkdir(path)
177
+ Dir.chdir(path) do
178
+ yield
179
+ end
180
+
181
+ FileUtils.remove_entry(path)
182
+ end
183
+
170
184
  private
171
185
 
172
186
  def new_temp_path
@@ -183,20 +197,29 @@ module Armature
183
197
  "#{@path}/object/#{@process_prefix}.#{@sequence}#{name}"
184
198
  end
185
199
 
186
- def fs_sanitize(ref)
200
+ def fs_repo_id(repo)
201
+ _fs_repo_id(repo.type, repo.url)
202
+ end
203
+
204
+ def _fs_repo_id(type, url)
205
+ fs_sanitize("#{type}:#{url}")
206
+ end
207
+
208
+ ### FIXME write tests for this
209
+ def fs_sanitize(str)
187
210
  # Escape | and replace / with |. Also escape a leading .
188
- ref.sub(/\A\./, "\\.").gsub(/[\\|]/, '\\\0').gsub(/\//, '|')
211
+ str.sub(/\A\./, "\\.").gsub(/[\\|]/, '\\\0').gsub(%r{/}, '|')
189
212
  end
190
213
 
191
- def fs_unsanitize(name)
192
- name.gsub(/\|/, '/').gsub(/\\([\\|])/, '\0').sub(/^\\\./, '.')
214
+ ### FIXME write tests for this
215
+ def fs_unsanitize(str)
216
+ str.gsub(/\\([\\|])/, '\0').gsub(/\|/, '/').sub(/^\A\\\./, '.')
193
217
  end
194
218
 
195
- # Assumes identity exists. Use checkout() if it might not.
196
- def checkout_identity(repo, identity)
219
+ def open_identity(repo, identity)
197
220
  safe_identity = fs_sanitize(identity)
198
221
 
199
- repo_path = "#{@path}/identity/#{repo.name}"
222
+ repo_path = "#{@path}/ref/identity/#{fs_repo_id(repo)}"
200
223
  identity_path = "#{repo_path}/#{safe_identity}"
201
224
  if Dir.exist? identity_path
202
225
  return identity_path
@@ -204,7 +227,7 @@ module Armature
204
227
 
205
228
  FileUtils.mkdir_p(repo_path)
206
229
 
207
- Armature::Util::lock identity_path, File::LOCK_EX, "checkout" do
230
+ Armature::Util::lock(identity_path, File::LOCK_EX, "check out") do
208
231
  # Another process may have created the object before we got the lock
209
232
  if Dir.exist? identity_path
210
233
  return identity_path
@@ -214,11 +237,13 @@ module Armature
214
237
  FileUtils.mkdir_p object_path
215
238
 
216
239
  @logger.debug(
217
- "Checking out '#{identity}' from '#{repo.url}' into '#{object_path}'")
218
- repo.git "reset", "--hard", identity, :work_dir=>object_path
240
+ "Checking out '#{identity}' from '#{repo}' into '#{object_path}'")
241
+ yield object_path
219
242
  atomic_symlink(object_path, identity_path)
220
243
  end
221
244
 
245
+ @logger.debug("Finished checking out \"#{identity}\" from \"#{repo}\"")
246
+
222
247
  identity_path
223
248
  end
224
249
 
@@ -1,11 +1,37 @@
1
1
  module Armature
2
2
  class Environments
3
+ class InvalidNameError < Armature::Error
4
+ end
5
+
6
+ # The documentation claims that uppercase letters are invalid, but in
7
+ # practice they seem to be fine.
8
+ #
9
+ # https://docs.puppet.com/puppet/latest/reference/lang_reserved.html#environments
10
+ def self.valid_environment_name?(name)
11
+ name =~ /\A[A-Za-z0-9_]+\Z/
12
+ end
13
+
14
+ def self.assert_valid_environment_name(name)
15
+ if ! valid_environment_name?(name)
16
+ raise InvalidNameError, "Invalid environment name '#{name}'"
17
+ end
18
+ end
19
+
20
+ # Same rules as for a class
21
+ def self.assert_valid_module_name(name)
22
+ if name !~ /\A[a-z][a-z0-9_]*\Z/
23
+ raise InvalidNameError, "Invalid module name '#{name}'"
24
+ end
25
+ end
26
+
3
27
  attr_reader :path
28
+ attr_accessor :fix_environment_names
4
29
 
5
30
  # path is the path to the directory containing all the environments
6
31
  def initialize(path, cache)
7
32
  @cache = cache
8
33
  @logger = Logging.logger[self]
34
+ @fix_environment_names = false
9
35
 
10
36
  if not File.directory? path
11
37
  abort "Puppet environments path does not exist: '#{path}'"
@@ -25,23 +51,32 @@ module Armature
25
51
  @logger.debug "Environment '#{name}' does not exist"
26
52
  end
27
53
 
28
- def checkout_ref(repo, ref, name=ref)
29
- # This will add and update a modules dir in any repo, even if the repo is
30
- # used in a Puppetfile. (Perhaps the cache is used for multiple repos?)
31
-
32
- # https://docs.puppet.com/puppet/latest/reference/lang_reserved.html#environments
33
- if name !~ /\A[a-z0-9_]+\Z/
34
- raise "Invalid environment name '#{name}'"
54
+ def normalize_environment_name(name)
55
+ if @fix_environment_names && ! self.class.valid_environment_name?(name)
56
+ old = name
57
+ name = old.gsub(/[^A-Za-z0-9_]/, "_")
58
+ @logger.info("Changing invalid environment name \"#{old}\" to \"#{name}\"")
35
59
  end
36
60
 
61
+ self.class.assert_valid_environment_name(name)
62
+ name
63
+ end
64
+
65
+ # Create an environment from a ref
66
+ #
67
+ # This will add and update a modules dir in any repo, even if the repo is
68
+ # used in a Puppetfile. (Perhaps the cache is used for multiple repos?)
69
+ def check_out_ref(repo, ref_str, name=ref_str)
70
+ name = normalize_environment_name(name)
71
+
37
72
  @cache.lock File::LOCK_SH do
38
- @logger.info "Deploying ref '#{ref}' from '#{repo.url}' as" \
73
+ @logger.info "Deploying ref '#{ref_str}' from '#{repo}' as" \
39
74
  " environment '#{name}'"
40
75
 
41
76
  begin
42
- ref_path = @cache.checkout(repo, ref, true)
43
- rescue RefError
44
- @logger.info "Ref '#{ref}' does not exist; ensuring environment" \
77
+ ref_path = repo.general_ref(ref_str).check_out()
78
+ rescue Armature::RefError
79
+ @logger.info "Ref '#{ref_str}' does not exist; ensuring environment" \
45
80
  " '#{name}' is gone"
46
81
  remove(name)
47
82
  return
@@ -50,7 +85,7 @@ module Armature
50
85
  puppetfile_path = "#{ref_path}/Puppetfile"
51
86
  if File.exist?(puppetfile_path)
52
87
  @logger.debug "Found Puppetfile in environment '#{name}'"
53
- module_refs = Armature::Puppetfile.new().include(puppetfile_path)
88
+ module_refs = Armature::Puppetfile.new(@cache).include(puppetfile_path)
54
89
  @logger.debug "Loaded Puppetfile in environment '#{name}' with" \
55
90
  " #{module_refs.length} modules"
56
91
  else
@@ -60,8 +95,9 @@ module Armature
60
95
 
61
96
  update_modules(ref_path, module_refs)
62
97
 
98
+ # Make the change live
63
99
  @cache.atomic_symlink(ref_path, "#{@path}/#{name}")
64
- @logger.debug "Done deploying ref '#{ref}' from '#{repo.url}' as" \
100
+ @logger.debug "Done deploying ref '#{ref_str}' from '#{repo}' as" \
65
101
  " environment '#{name}'"
66
102
  end
67
103
  end
@@ -69,6 +105,8 @@ module Armature
69
105
  private
70
106
 
71
107
  # Apply the results of the Puppetfile to a ref (e.g. an environment)
108
+ #
109
+ ### FIXME This could update modules in an existing check out.
72
110
  def update_modules(target_path, module_refs)
73
111
  modules_path = "#{target_path}/modules"
74
112
  if ! Dir.exist? modules_path
@@ -76,14 +114,9 @@ module Armature
76
114
  end
77
115
 
78
116
  module_refs.each do |name, info|
79
- if name =~ /\A\./
80
- raise "Module name may not start with period: '#{name}'"
81
- elsif name =~ /\//
82
- raise "Module name may not contain /: '#{name}'"
83
- end
84
-
85
- ref_path = @cache.checkout(@cache.get_repo(info[:git]), info[:ref])
117
+ self.class.assert_valid_module_name(name)
86
118
 
119
+ ref_path = info[:ref].check_out()
87
120
  @cache.atomic_symlink(ref_path, "#{modules_path}/#{name}")
88
121
  end
89
122