Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

habitat_package: Add installation_path and dependency_ids properties #32

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/resources/habitat_package.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ end

Use [properties](https://www.inspec.io/docs/reference/glossary/#property) to create tests that compare an expected value to the actual value.

### dependency_ids

`Array` of `Strings`. Each entry is the full id (origin/name/version/release) of each dependency of the package. This includes transitive ("deep") dependencies. See also the [`dependency_names` property](#dependency_names).

```ruby
describe habitat_package('core/httpd') do
its('dependency_ids') { should_not include 'core/openssl/1.2.3/20190307151146' }
end
```

### dependency_names

`Array` of `Strings`. Each entry is the short name (origin/name) of each dependency of the package. This includes transitive ("deep") dependencies. See also the [`dependency_ids` property](#dependency_ids).

```ruby
describe habitat_package('core/httpd') do
its('dependency_names') { should_not include 'core/mod_lua' }
end
```

### identifier

`String`. The origin, name, version (if known) and release (if known) concatenated with `/`, to create the package identifier.
Expand All @@ -139,6 +159,16 @@ describe habitat_package(origin: 'core', name: 'httpd') do
end
```

### installation_path

`String`. The absolute path to the directory under which the package is installed.

```ruby
describe habitat_package(origin: 'core', name: 'httpd') do
its('installation_path') { should begin_with '/opt' }
end
```

### name

`String`. The name of the package, as passed in via the resource parameter. Always available, even if the resource was not found. See also [origin](#origin) and [version](#version).
Expand Down
48 changes: 47 additions & 1 deletion libraries/habitat_package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,57 @@ def identifier
end

def to_s
"Habitat Service #{identifier}"
"Habitat Package #{identifier}"
end

def installation_path
return nil unless exists?

"#{hab_install_root}/#{identifier}"
end

# Read dependency package IDs from the TDEPs file
def dependency_ids
return [] unless exists?
return @dependency_ids if defined?(@dependency_ids)

tdeps = inspec.backend.run_command("cat #{installation_path}/TDEPS").stdout
@dependency_ids = tdeps.chomp.split("\n")
end

def dependency_names
return [] unless exists?
return @dependency_names if defined?(@dependency_names)

@dependency_names = dependency_ids.map { |id| id.split('/')[0, 2].join('/') }
end

private

def hab_install_root
@hab_install_root ||= determine_hab_install_root
end

# Figure out the hab installation path by looking at the PATH of
# a package known to be installed, and known to have a simple PATH
# - hab itself is perfect for this.
def determine_hab_install_root
list_result = inspec.backend.run_hab_cli('pkg list core/hab')
hab_spec = list_result.stdout.split("\n").first

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stdout.lines.first is more expressive.

env_result = inspec.backend.run_hab_cli("pkg env #{hab_spec}")
path_line = env_result.stdout.split("\n").detect { |l| l.include?('PATH') }
path_line.tr!('\\', '/') # Force slashes to be backslashes to match package IDs
# Like export PATH="/hab/pkgs/core/hab/0.81.0/20190507225645/bin"
# Or set PATH="C:\hab\pkgs\core\hab\0.81.0\20190507225645\bin"
match = path_line.match(%r{="(.+)[\\\/]#{hab_spec}})
unless match
# TODO: Inspec 3174 resource unable handling
raise Inspec::Exceptions::ResourceFailed, 'Cannot determine habitat install root'
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For something this simple, unless the line is long/ugly, I use an unless modifier. (X unless Y)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was getting confused by this code and match[1] until my brain flipped on and realized this was just a simple substring regexp match. While =~ is more perl-like, it is more idiomatic ruby and it is measurably faster (it isn't a normal method call and it doesn't create that match object unless it is accessed) so use it and $1:

path_line =~ %r%="(.+)[\\\/]#{hab_spec}%
raise [...] unless $1
$1

I only flipped it to %r%...% out of preference. I like that % has a forward slash so it looks like clean alternative.


match[1]
end

def perform_existence_check
unless inspec.backend.cli_options_provided?
@exists = false
Expand Down
2 changes: 2 additions & 0 deletions test/integration/sup-fixture/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ systemctl start habitat
systemctl enable habitat
fi

hab license accept

# Install and start two services
echo "Installing and starting core/httpd"
hab pkg install "core/httpd"
Expand Down
3 changes: 3 additions & 0 deletions test/integration/test-profile/controls/habitat_package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
it { should exist }
its('version') { should cmp >= '2.3.45' }
its('release') { should cmp >= '20180608050617' }
its('installation_path') { should match %r(/hab/pkgs/core/httpd/\d+\.\d+\.\d+/\d{14}) }
its('dependency_ids.count') { should cmp >= 28 }
its('dependency_names') { should include 'core/glibc' }
end
end
28 changes: 28 additions & 0 deletions test/unit/fixtures/cat-httpd-tdeps.cli.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
core/acl/2.2.53/20190115012136
core/apr-util/1.6.1/20190307150947
core/apr/1.6.5/20190307150747
core/attr/2.4.48/20190115012129
core/bash/4.4.19/20190115012619
core/binutils/2.31.1/20190115003743
core/bzip2/1.0.6/20190115011950
core/cacerts/2018.12.05/20190115014206
core/coreutils/8.30/20190115012313
core/db/5.3.28/20190115012845
core/expat/2.2.5/20190115012836
core/gcc-libs/8.2.0/20190115011926
core/gdbm/1.17/20190115012826
core/glibc/2.27/20190115002733
core/gmp/6.1.2/20190115003943
core/grep/3.1/20190115012541
core/less/530/20190115013008
core/libcap/2.25/20190115012150
core/libiconv/1.14/20190115155025
core/linux-headers/4.17.12/20190115002705
core/ncurses/6.1/20190115012027
core/openssl-fips/2.0.16/20190115014207
core/openssl/1.0.2r/20190305210149
core/pcre/8.42/20190115012526
core/perl/5.28.0/20190115013014
core/readline/7.0.3/20190115012607
core/sed/4.5/20190115012152
core/zlib/1.2.11/20190115003728
1 change: 1 addition & 0 deletions test/unit/fixtures/pkg-env-hab.cli.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export PATH="/hab/pkgs/core/hab/0.81.0/20190507225645/bin"
1 change: 1 addition & 0 deletions test/unit/fixtures/pkg-list-hab.cli.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
core/hab/0.81.0/20190507225645
45 changes: 45 additions & 0 deletions test/unit/package_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,50 @@
end
end
end

#==========================================================================#
# Properties
#==========================================================================#

describe 'properties' do
describe 'installation_path' do
it 'should be correct' do
fixture = {
cli: [
{ cmd: 'pkg list core/hab', stdout_file: 'pkg-list-hab.cli.txt' },
{ cmd: regexp_matches(%r{pkg env core/hab}), stdout_file: 'pkg-env-hab.cli.txt' },
{ cmd: 'pkg list core/httpd', stdout_file: 'pkg-list-single.cli.txt' },
],
}
InspecHabitat::UnitTestHelper.mock_inspec_context_object(self, fixture)
pkg = HabitatPackage.new('core/httpd')
pkg.installation_path.must_equal('/hab/pkgs/core/httpd/2.4.35/20190307151146')
end
end

describe 'dependency ids and names' do
it 'should be correct' do
fixture = {
cli: [
{ cmd: 'pkg list core/hab', stdout_file: 'pkg-list-hab.cli.txt' },
{ cmd: regexp_matches(%r{pkg env core/hab}), stdout_file: 'pkg-env-hab.cli.txt' },
{ cmd: 'pkg list core/httpd', stdout_file: 'pkg-list-single.cli.txt' },
],
general_cli: [
{ cmd: 'cat /hab/pkgs/core/httpd/2.4.35/20190307151146/TDEPS', stdout_file: 'cat-httpd-tdeps.cli.txt' },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these magic strings be pulled out into constants or anything? 🤷‍♀

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably, it references a package version and date, eventually it might not be in the package index.

],
}
InspecHabitat::UnitTestHelper.mock_inspec_context_object(self, fixture)
pkg = HabitatPackage.new('core/httpd')
dep_ids = pkg.dependency_ids
dep_ids.count.must_equal 28
dep_ids.must_include 'core/glibc/2.27/20190115002733'

dep_names = pkg.dependency_names
dep_names.count.must_equal dep_ids.count
dep_names.must_include 'core/glibc'
end
end
end
end
# rubocop:enable Metrics/BlockLength
46 changes: 36 additions & 10 deletions test/unit/unit_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def check_definition(opts)
# Pass a fixture Hash to this method as the second arg.
# {
# cli: { # Optional. If present, connection will say that CLI mode is available.
# # This may also be an array of such hashes.
# cmd: 'svc status core/httpd', # This registers the stub, so it will only respond to this command
# stdout_file: 'svc-status-single.cli.txt', # A file under test/unit/fixtures, empty String if this key is absent
# stderr_file: 'some-other-file.cli.txt', # A file under test/unit/fixtures, empty String if this key is absent
Expand All @@ -45,7 +46,10 @@ def check_definition(opts)
# path: '/services', # This registers the stub, so it will only respond to this path
# body_file: 'services-single.api.json', # A file under test/unit/fixtures, empty String if this key is absent
# code: 200
# }
# },
# general_cli: [ # Used for general run_command CLI calls.
# { cmd: '', stdout_file: '' }, # As for cli: above
# ],
# }

# About this method.
Expand All @@ -57,7 +61,10 @@ def check_definition(opts)
# which means we need to be in an `it` block. So... and this is awful ...
# pass the it block (which is `self`, within the it block) as the test
# context and then perform a block-type instance-eval.
def mock_inspec_context_object(test_cxt, fixture) # rubocop:disable Metrics/AbcSize

# Also, this is really bad, linting-wise; but I'm not sure how to refactor
# this much, in an instance eval....
def mock_inspec_context_object(test_cxt, fixture) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hah! I love the rubycop:disable part!

test_cxt.instance_eval do
inspec_cxt = mock
hab_cxn = mock
Expand All @@ -66,15 +73,19 @@ def mock_inspec_context_object(test_cxt, fixture) # rubocop:disable Metrics/AbcS

if fixture.key?(:cli)
hab_cxn.stubs(:cli_options_provided?).returns(true)
run_result = mock
run_result.stubs(:exit_status).returns(fixture[:cli][:exit_status])

out = fixture[:cli][:stdout_file] ? File.read(File.join(unit_fixture_path, fixture[:cli][:stdout_file])) : ''
run_result.stubs(:stdout).returns(out)
err = fixture[:cli][:stderr_file] ? File.read(File.join(unit_fixture_path, fixture[:cli][:stderr_file])) : ''
run_result.stubs(:stderr).returns(err)
# Boost this to an Array if it isn't already
(fixture[:cli].is_a?(Array) ? fixture[:cli] : [fixture[:cli]]).each do |cli_fixture|

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(fixture[:cli].is_a?(Array) ? fixture[:cli] : [fixture[:cli]]).each ...

to:

Array(fixture[:cli]).each ...

run_result = mock
run_result.stubs(:exit_status).returns(cli_fixture[:exit_status] || 0)

out = cli_fixture[:stdout_file] ? File.read(File.join(unit_fixture_path, cli_fixture[:stdout_file])) : ''

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor these into a helper method or two.

run_result.stubs(:stdout).returns(out)
err = cli_fixture[:stderr_file] ? File.read(File.join(unit_fixture_path, cli_fixture[:stderr_file])) : ''
run_result.stubs(:stderr).returns(err)

hab_cxn.stubs(:run_hab_cli).with(fixture[:cli][:cmd]).returns(run_result)
hab_cxn.stubs(:run_hab_cli).with(cli_fixture[:cmd]).returns(run_result)
end
else
hab_cxn.stubs(:cli_options_provided?).returns(false)
end
Expand All @@ -93,8 +104,23 @@ def mock_inspec_context_object(test_cxt, fixture) # rubocop:disable Metrics/AbcS
else
hab_cxn.stubs(:api_options_provided?).returns(false)
end
Inspec::Plugins::Resource.any_instance.stubs(:inspec).returns(inspec_cxt)

if fixture.key?(:general_cli)
fixture[:general_cli].each do |cli_fixture|
hab_cxn.stubs(:cli_options_provided?).returns(true)
run_result = mock
run_result.stubs(:exit_status).returns(cli_fixture[:exit_status] || 0)
out = cli_fixture[:stdout_file] ? File.read(File.join(unit_fixture_path, cli_fixture[:stdout_file])) : ''
run_result.stubs(:stdout).returns(out)
err = cli_fixture[:stderr_file] ? File.read(File.join(unit_fixture_path, cli_fixture[:stderr_file])) : ''
run_result.stubs(:stderr).returns(err)
hab_cxn.stubs(:run_command).with(cli_fixture[:cmd]).returns(run_result)
end
end

# Stub at both instance and class level
Inspec::Plugins::Resource.any_instance.stubs(:inspec).returns(inspec_cxt)
Inspec::Plugins::Resource.stubs(:inspec).returns(inspec_cxt)
end
end
module_function :mock_inspec_context_object # rubocop:disable Style/AccessModifierDeclarations
Expand Down