Class: PolicyChangelog

Inherits:
Object
  • Object
show all
Defined in:
lib/knife/changelog/policyfile.rb

Constant Summary collapse

TMP_PREFIX =
'knife-changelog'
VERSION_REGEX =

Regex matching Chef cookbook version syntax See docs.chef.io/cookbook_versioning.html#syntax

/^[1-9]*[0-9](\.[0-9]+){1,2}$/

Instance Method Summary collapse

Constructor Details

#initialize(cookbooks, policyfile, with_dependencies) ⇒ PolicyChangelog

Initialzes Helper class

Parameters:

  • cookbooks (Array<String>)

    cookbooks to update (@name_args)

  • policyfile (String)

    policyfile path



22
23
24
25
26
27
# File 'lib/knife/changelog/policyfile.rb', line 22

def initialize(cookbooks, policyfile, with_dependencies)
  @cookbooks_to_update = cookbooks
  @policyfile_path = File.expand_path(policyfile)
  @policyfile_dir = File.dirname(@policyfile_path)
  @with_dependencies = with_dependencies
end

Instance Method Details

#format_output(name, data) ⇒ String

Formats commit changelog to be more readable

Parameters:

  • name (String)

    cookbook name

  • data (Hash)

    cookbook versions and source url data

Returns:

  • (String)

    formatted changelog



179
180
181
182
183
184
185
186
187
188
189
# File 'lib/knife/changelog/policyfile.rb', line 179

def format_output(name, data)
  output = ["\nChangelog for #{name}: #{data['current_version']}->#{data['target_version']}"]
  output << '=' * output.first.size
  output << if data['current_version']
              git_changelog(data['source_url'], data['current_version'], data['target_version'], name)
            else
              'Cookbook was not in the Policyfile.lock.json'
            end

  output.join("\n")
end

#generate_changelog(prevent_downgrade: false) ⇒ String

Generates Policyfile changelog

Returns:

  • (String)

    formatted version changelog



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/knife/changelog/policyfile.rb', line 217

def generate_changelog(prevent_downgrade: false)
  lock_current = read_policyfile_lock(@policyfile_dir)
  current = versions(lock_current['cookbook_locks'], 'current')

  lock_target = update_policyfile_lock
  target = versions(lock_target['cookbook_locks'], 'target')

  updated_cookbooks = current.deep_merge(target).reject { |_name, data| reject_version_filter(data) }
  changelog_cookbooks = if @with_dependencies || @cookbooks_to_update.nil?
                          updated_cookbooks
                        else
                          updated_cookbooks.select { |name, _data| @cookbooks_to_update.include?(name) }
                        end

  validate_downgrade!(updated_cookbooks) if prevent_downgrade

  generate_changelog_from_versions(changelog_cookbooks)
end

#generate_changelog_from_versions(cookbook_versions) ⇒ String

Generates Policyfile changelog

Parameters:

  • cookbook_versions.

    Format is { ‘NAME’ => { ‘current_version’ => ‘VERSION’, ‘target_version’ => ‘VERSION’ }

Returns:

  • (String)

    formatted version changelog



240
241
242
243
244
245
246
# File 'lib/knife/changelog/policyfile.rb', line 240

def generate_changelog_from_versions(cookbook_versions)
  lock_current = read_policyfile_lock(@policyfile_dir)
  sources = cookbook_versions.map do |name, data|
    [name, get_source_url(lock_current['cookbook_locks'][name]['source_options'])] if data['current_version']
  end.compact.to_h
  cookbook_versions.deep_merge(sources).map { |name, data| format_output(name, data) }.join("\n")
end

#get_source_url(s) ⇒ String

Extracts Git source URL from cookbook ‘source_options’ data depending on the source type - Supermarket or Git

Parameters:

  • s (Hash)

    source_options for a cookbook in the Policyfile.lock

Returns:

  • (String)

    Git source code URL



81
82
83
84
85
86
87
# File 'lib/knife/changelog/policyfile.rb', line 81

def get_source_url(s)
  if s.keys.include?('artifactserver')
    { 'source_url' => supermarket_source_url(s['artifactserver'][%r{(.+)\/versions\/.*}, 1]) }
  else
    { 'source_url' => s['git'] }
  end
end

#git_changelog(source_url, current, target, cookbook = nil) ⇒ String

Clones a Git repo in a temporary directory and generates a commit changelog between two version tags

Parameters:

  • source_url (String)

    Git repository URL

  • current (String)

    current cookbook version tag

  • target (String)

    target cookbook version tag

Returns:

  • (String)

    changelog between tags for one cookbook



110
111
112
113
114
115
116
117
# File 'lib/knife/changelog/policyfile.rb', line 110

def git_changelog(source_url, current, target, cookbook = nil)
  dir = Dir.mktmpdir(TMP_PREFIX)
  repo = Git.clone(source_url, dir)
  cookbook_path = cookbook ? git_cookbook_path(repo, cookbook) : '.'
  repo.log.path(cookbook_path).between(git_ref(current, repo, cookbook), git_ref(target, repo, cookbook)).map do |commit|
    "#{commit.sha[0, 7]} #{commit.message.lines.first.strip}"
  end.join("\n")
end

#git_cookbook_path(repo, cookbook) ⇒ String

Tries to find the location of a specific cookbook in the given repo

Parameters:

  • repo (Git::Base)

    Git repository object

  • cookbook (String)

    name of the cookbook to search the location

Returns:

  • (String)

    reative location of the cookbook in the repo



124
125
126
127
128
129
130
131
132
# File 'lib/knife/changelog/policyfile.rb', line 124

def git_cookbook_path(repo, cookbook)
   = ['metadata.rb', '*/metadata.rb'].flat_map { |location| repo.ls_files(location).keys }
   = .find do |path|
    path = ::File.join(repo.dir.to_s, path)
    ::Chef::Cookbook::Metadata.new.tap { |m| m.from_file(path) }.name == cookbook
  end
  raise "Impossible to find matching metadata for #{cookbook} in #{repo.remote.url}" unless 
  ::File.dirname()
end

#git_ref(myref, repo, cookbook_name = nil) ⇒ String

Tries to convert a supermarket tag to a git reference if there is a difference in formatting between the two. This is issue is present for the ‘java’ cookbook. github.com/agileorbit-cookbooks/java/issues/450

Parameters:

  • ref (String)

    version reference

  • repo (Git::Base)

    Git repository object

  • cookbook (String)

    name of the cookbook to ref against

Returns:

  • (String)


143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/knife/changelog/policyfile.rb', line 143

def git_ref(myref, repo, cookbook_name = nil)
  possible_refs = ['v' + myref, myref]
  possible_refs += possible_refs.map { |ref| "#{cookbook_name}-#{ref}" } if cookbook_name
  possible_refs += possible_refs.map { |ref| ref.chomp('.0') } if myref[/\.0$/]
  existing_ref = possible_refs.find do |ref|
    begin
      repo.checkout(ref)
    rescue ::Git::GitExecuteError
      false
    end
  end
  raise "Impossible to find existing references to #{possible_refs} in #{repo.remote.url}" unless existing_ref
  existing_ref
end

#read_policyfile_lock(dir) ⇒ Hash

Parses JSON in Policyfile.lock.

Parameters:

  • dir (String)

    directory containing Policyfile.lock

Returns:

  • (Hash)

    contents of Policyfile.lock



48
49
50
51
52
53
54
# File 'lib/knife/changelog/policyfile.rb', line 48

def read_policyfile_lock(dir)
  lock = File.join(dir, 'Policyfile.lock.json')
  raise "File #{lock} does not exist" unless File.exist?(lock)
  content = JSON.parse(File.read(lock))
  raise 'Policyfile.lock empty' if content.empty?
  content
end

#reject_version_filter(data) ⇒ true, false

Filters out cookbooks which are not updated, are not used after update

Parameters:

  • cookbook (Hash)

    versions and source url data

Returns:

  • (true, false)


195
196
197
198
# File 'lib/knife/changelog/policyfile.rb', line 195

def reject_version_filter(data)
  raise 'Data containing versions is nil' if data.nil?
  data['current_version'] == data['target_version'] || data['target_version'].nil?
end

#sort_by_version(tags) ⇒ Array

Sort tags by version and filter out invalid version tags

Parameters:

  • tags (Array<Git::Object::Tag>)

    git tags

Returns:

  • (Array)

    git tags sorted by version



162
163
164
165
166
167
168
169
170
171
172
# File 'lib/knife/changelog/policyfile.rb', line 162

def sort_by_version(tags)
  tags.sort_by do |t|
    begin
      Gem::Version.new(t.name.gsub(/^v/, ''))
    rescue ArgumentError => e
      # Skip tag if version is not valid (i.e. a String)
      raise unless e.message && e.message.include?('Malformed version number string')
      Gem::Version.new('0.0.0')
    end
  end
end

#supermarket_source_url(url) ⇒ String

Fetches cookbook metadata from Supermarket and extracts Git source URL

Parameters:

  • url (String)

    Supermarket cookbook URL

Returns:

  • (String)

    Git source code URL



93
94
95
96
97
98
99
100
101
# File 'lib/knife/changelog/policyfile.rb', line 93

def supermarket_source_url(url)
  source_url = JSON.parse(RestClient::Request.execute(
                            url: url,
                            method: :get,
                            verify_ssl: false
  ))['source_url']
  source_url = "#{source_url}.git" unless source_url.end_with?('.git')
  source_url
end

#update_policyfile_lockObject

Updates the Policyfile.lock to get version differences.



32
33
34
35
36
37
38
39
40
41
42
# File 'lib/knife/changelog/policyfile.rb', line 32

def update_policyfile_lock
  backup_dir = Dir.mktmpdir
  FileUtils.cp(File.join(@policyfile_dir, 'Policyfile.lock.json'), backup_dir)
  installer = ChefCLI::Command::Install.new
  raise "Cannot install Policyfile lock #{@policyfile_path}" unless installer.run([@policyfile_relative_path]).zero?
  updater = ChefCLI::Command::Update.new
  raise "Error updating Policyfile lock #{@policyfile_path}" unless updater.run([@policyfile_path, @cookbooks_to_update].flatten).zero?
  updated_policyfile_lock = read_policyfile_lock(@policyfile_dir)
  FileUtils.cp(File.join(backup_dir, 'Policyfile.lock.json'), @policyfile_dir)
  updated_policyfile_lock
end

#validate_downgrade!(data) ⇒ Object

Search for cookbook downgrade and raise an error if any



201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/knife/changelog/policyfile.rb', line 201

def validate_downgrade!(data)
  downgrade = data.select do |_, ck|
    # Do not try to validate downgrade on non-sementic versions (e.g. git revision)
    ck['target_version'] =~ VERSION_REGEX && ck['current_version'] =~ VERSION_REGEX &&
      ::Gem::Version.new(ck['target_version']) < ::Gem::Version.new(ck['current_version'])
  end

  return if downgrade.empty?

  details = downgrade.map { |name, data| "#{name} (#{data['current_version']} -> #{data['target_version']})" }
  raise "Trying to downgrade following cookbooks: #{details.join(', ')}"
end

#versions(locks, type) ⇒ Hash

Extracts current or target versions from Policyfile.lock data depending on the type value provided.

Parameters:

  • locks (Hash)

    cookbook data from Policyfile.lock

  • type (String)

    version type - current or target

Returns:

  • (Hash)

    cookbooks with their versions



62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/knife/changelog/policyfile.rb', line 62

def versions(locks, type)
  raise 'Use "current" or "target" as type' unless %w[current target].include?(type)
  raise 'Cookbook locks empty or nil' if locks.nil? || locks.empty?
  cookbooks = {}
  locks.each do |name, data|
    cookbooks[name] = if data['source_options'].keys.include?('git')
                        { "#{type}_version" => data['source_options']['revision'] }
                      else
                        { "#{type}_version" => data['version'] }
                      end
  end
  cookbooks
end