inspec-docker-resources 7.1.5

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 42403a609f1a1f69bef2e78bfd0ccd689572c3e1f97410bdd2a255edb25deb89
4
+ data.tar.gz: 806499cbed562f28c5797e2496ed2e816ab90fc24e34011791bcefacd0fae3a7
5
+ SHA512:
6
+ metadata.gz: 7e5690271746f476fb40504d742924f93c5f5d3460ca74a9168f4270fe813e73bd33d818e5907ad0875319e3a0ec49f86a4625fe9f3b5351481f2b577ac95c9b
7
+ data.tar.gz: a5c00db497287e910beb8ffa0a6a37382dc8ba5799b1cba2253e0994cdf3f7d78d43b533be27e6bd50b590b0afebe4c41d40cc4fd4f0af520d5f6784c5b9ac72
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+ gem "inspec-bin", git: "https://github.com/inspec/inspec", branch: "inspec-7"
3
+ gem "inspec", git: "https://github.com/inspec/inspec", branch: "inspec-7"
4
+
5
+ gemspec
6
+
7
+ group :test do
8
+ gem "mocha"
9
+ gem "minitest"
10
+ gem "mocha"
11
+ gem "chefstyle"
12
+ gem "simplecov"
13
+ gem "simplecov_json_formatter"
14
+ end
data/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # inspec-docker-resources
2
+
3
+ Docker InSpec Resources in a Gem
4
+
5
+ This repository contains the InSpec Docker resources, formerly contained in InSpec Core. In InSpec 7+, these resources are available in a gem, `inspec-docker-resources`.
6
+
7
+ ## Usage
8
+
9
+ To use this resource pack, add this dependency to your inspec.yml :
10
+
11
+ ```yaml
12
+ depends:
13
+ - name: inspec-docker-resources
14
+ gem: inspec-docker-resources
15
+ ```
16
+
@@ -0,0 +1,44 @@
1
+ # As plugins are usually packaged and distributed as a RubyGem,
2
+ # we have to provide a .gemspec file, which controls the gembuild
3
+ # and publish process. This is a fairly generic gemspec.
4
+
5
+ # It is traditional in a gemspec to dynamically load the current version
6
+ # from a file in the source tree. The next three lines make that happen.
7
+ lib = File.expand_path("lib", __dir__)
8
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
9
+ require "inspec-docker-resources/version"
10
+
11
+ Gem::Specification.new do |spec|
12
+ # Importantly, all InSpec plugins must be prefixed with `inspec-` (most
13
+ # plugins) or `train-` (plugins which add new connectivity features).
14
+ spec.name = "inspec-docker-resources"
15
+
16
+ # It is polite to namespace your plugin under InspecPlugins::YourPluginInCamelCase
17
+ spec.version = InspecPlugins::DockerResources::VERSION
18
+ spec.authors = ["InSpec Core Maintainers"]
19
+ spec.email = ["inspec@progress.com"]
20
+ spec.summary = "Docker InSpec Resources in a Gem"
21
+ spec.description = "Contains InSpec 7.0+ resources fo interacting with Docker Desktop."
22
+ spec.homepage = "https://github.com/inspec/inspec-docker-resources"
23
+ spec.license = "Apache-2.0"
24
+
25
+ # Though complicated-looking, this is pretty standard for a gemspec.
26
+ # It just filters what will actually be packaged in the gem (leaving
27
+ # out tests, etc)
28
+ spec.files = %w{
29
+ README.md inspec-docker-resources.gemspec Gemfile inspec.yml
30
+ } + Dir.glob(
31
+ "lib/**/*", File::FNM_DOTMATCH
32
+ ).reject { |f| File.directory?(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.required_ruby_version = ">= 3.1.0"
36
+
37
+ # If you rely on any other gems, list them here with any constraints.
38
+ # This is how `inspec plugin install` is able to manage your dependencies.
39
+ # For example, perhaps you are writing a thing that talks to AWS, and you
40
+ # want to ensure you have `aws-sdk` in a certain version.
41
+
42
+ # This plugin uses InSpec 7 Resource Pack Plugins
43
+ spec.add_dependency "inspec-core", ">= 7.0"
44
+ end
data/inspec.yml ADDED
@@ -0,0 +1,10 @@
1
+ name: inspec-docker-resources
2
+ title: InSpec Docker Resources
3
+ maintainer: InSpec Core Maintainers
4
+ copyright: Progress Software Corporation
5
+ copyright_email: inspec@progress.com
6
+ license: Apache-2.0
7
+ summary: Docker InSpec Resources in a Gem
8
+ version: 7.1.5
9
+ supports:
10
+ platform: os
@@ -0,0 +1,42 @@
1
+ # Plugin Definition file
2
+ # The purpose of this file is to declare to InSpec what plugin_types (capabilities)
3
+ # are included in this plugin, and provide activator that will load them as needed.
4
+
5
+ # It is important that this file load successfully and *quickly*.
6
+ # Your plugin's functionality may never be used on this InSpec run; so we keep things
7
+ # fast and light by only loading heavy things when they are needed.
8
+
9
+ # Presumably this is light
10
+ require "inspec-docker-resources/version"
11
+
12
+ # The InspecPlugins namespace is where all plugins should declare themselves.
13
+ # The "Inspec" capitalization is used throughout the InSpec source code; yes, it's
14
+ # strange.
15
+ module InspecPlugins
16
+ # Pick a reasonable namespace here for your plugin. A reasonable choice
17
+ # would be the CamelCase version of your plugin gem name.
18
+ # inspec-test-resources => TestResources
19
+ module DockerResources
20
+ # This simple class handles the plugin definition, so calling it simply Plugin is OK.
21
+ # Inspec.plugin returns various Classes, intended to be superclasses for various
22
+ # plugin components. Here, the one-arg form gives you the Plugin Definition superclass,
23
+ # which mainly gives you access to the activator / plugin_type DSL.
24
+ # The number '2' says you are asking for version 2 of the plugin API. If there are
25
+ # future versions, InSpec promises plugin API v2 will work for at least two more InSpec
26
+ # major versions.
27
+ class Plugin < ::Inspec.plugin(2)
28
+ # Internal machine name of the plugin. InSpec will use this in errors, etc.
29
+ plugin_name :"inspec-docker-resources"
30
+
31
+ # Define a new Resource Pack.
32
+ resource_pack :"inspec-docker-resources" do
33
+ # This file will load the resources implicitly via the superclass
34
+ require "inspec-docker-resources/resource_pack"
35
+
36
+ # Having loaded our functionality, return a class that represents the plugin.
37
+ # Reserved for future use.
38
+ InspecPlugins::DockerResources::ResourcePack
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ require "inspec/resource"
2
+
3
+ module InspecPlugins::DockerResources
4
+ # This class will provide the actual CLI implementation.
5
+ # Its superclass is provided by another call to Inspec.plugin,
6
+ # this time with two args. The first arg specifies we are requesting
7
+ # version 2 of the Plugins API. The second says we are making a Resource
8
+ # Pack plugin component, so please make available any DSL needed
9
+ # for that.
10
+ class ResourcePack < Inspec.plugin(2, :resource_pack)
11
+ # TBD
12
+ # load_timing :early <-- isn't that implicit in the rewuire statements
13
+ # train relationship declarations? <-- that should be in the gemspec
14
+ end
15
+ end
File without changes
@@ -0,0 +1,272 @@
1
+ #
2
+ # Copyright 2017, Christoph Hartmann
3
+ #
4
+
5
+ require "inspec/resources/command"
6
+ require "inspec/utils/filter"
7
+ require "hashie/mash"
8
+
9
+ class DockerContainerFilter
10
+ # use filtertable for containers
11
+ filter = FilterTable.create
12
+ filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
13
+ filter.register_column(:commands, field: "command")
14
+ .register_column(:ids, field: "id")
15
+ .register_column(:images, field: "image")
16
+ .register_column(:labels, field: "labels", style: :simple)
17
+ .register_column(:local_volumes, field: "localvolumes")
18
+ .register_column(:mounts, field: "mounts")
19
+ .register_column(:names, field: "names")
20
+ .register_column(:networks, field: "networks")
21
+ .register_column(:ports, field: "ports")
22
+ .register_column(:running_for, field: "runningfor")
23
+ .register_column(:sizes, field: "size")
24
+ .register_column(:status, field: "status")
25
+ .register_custom_matcher(:running?) do |x|
26
+ x.where { status.downcase.start_with?("up") }
27
+ end
28
+ filter.install_filter_methods_on_resource(self, :containers)
29
+
30
+ attr_reader :containers
31
+ def initialize(containers)
32
+ @containers = containers
33
+ end
34
+ end
35
+
36
+ class DockerImageFilter
37
+ filter = FilterTable.create
38
+ filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
39
+ filter.register_column(:ids, field: "id")
40
+ .register_column(:repositories, field: "repository")
41
+ .register_column(:tags, field: "tag")
42
+ .register_column(:sizes, field: "size")
43
+ .register_column(:digests, field: "digest")
44
+ .register_column(:created, field: "createdat")
45
+ .register_column(:created_since, field: "createdsize")
46
+ filter.install_filter_methods_on_resource(self, :images)
47
+
48
+ attr_reader :images
49
+ def initialize(images)
50
+ @images = images
51
+ end
52
+ end
53
+
54
+ class DockerPluginFilter
55
+ filter = FilterTable.create
56
+ filter.add(:ids, field: "id")
57
+ .add(:names, field: "name")
58
+ .add(:versions, field: "version")
59
+ .add(:enabled, field: "enabled")
60
+ filter.connect(self, :plugins)
61
+
62
+ attr_reader :plugins
63
+ def initialize(plugins)
64
+ @plugins = plugins
65
+ end
66
+ end
67
+
68
+ class DockerServiceFilter
69
+ filter = FilterTable.create
70
+ filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
71
+ filter.register_column(:ids, field: "id")
72
+ .register_column(:names, field: "name")
73
+ .register_column(:modes, field: "mode")
74
+ .register_column(:replicas, field: "replicas")
75
+ .register_column(:images, field: "image")
76
+ .register_column(:ports, field: "ports")
77
+ filter.install_filter_methods_on_resource(self, :services)
78
+
79
+ attr_reader :services
80
+ def initialize(services)
81
+ @services = services
82
+ end
83
+ end
84
+
85
+ # This resource helps to parse information from the docker host
86
+ # For compatability with Serverspec we also offer the following resouses:
87
+ # - docker_container
88
+ # - docker_image
89
+ class Docker < Inspec.resource(1)
90
+ name "docker"
91
+ supports platform: "unix"
92
+ desc "
93
+ A resource to retrieve information about docker
94
+ "
95
+
96
+ example <<~EXAMPLE
97
+ describe docker.containers do
98
+ its('images') { should_not include 'u12:latest' }
99
+ end
100
+
101
+ describe docker.images do
102
+ its('repositories') { should_not include 'inssecure_image' }
103
+ end
104
+
105
+ describe docker.plugins.where { name == 'rexray/ebs' } do
106
+ it { should exist }
107
+ end
108
+
109
+ describe docker.services do
110
+ its('images') { should_not include 'inssecure_image' }
111
+ end
112
+
113
+ describe docker.version do
114
+ its('Server.Version') { should cmp >= '1.12'}
115
+ its('Client.Version') { should cmp >= '1.12'}
116
+ end
117
+
118
+ describe docker.object(id) do
119
+ its('Configuration.Path') { should eq 'value' }
120
+ end
121
+
122
+ docker.containers.ids.each do |id|
123
+ # call docker inspect for a specific container id
124
+ describe docker.object(id) do
125
+ its(%w(HostConfig Privileged)) { should cmp false }
126
+ its(%w(HostConfig Privileged)) { should_not cmp true }
127
+ end
128
+ end
129
+ EXAMPLE
130
+
131
+ def containers
132
+ DockerContainerFilter.new(parse_containers)
133
+ end
134
+
135
+ def images
136
+ DockerImageFilter.new(parse_images)
137
+ end
138
+
139
+ def plugins
140
+ DockerPluginFilter.new(parse_plugins)
141
+ end
142
+
143
+ def services
144
+ DockerServiceFilter.new(parse_services)
145
+ end
146
+
147
+ def version
148
+ return @version if defined?(@version)
149
+
150
+ data = {}
151
+ cmd = inspec.command("docker version --format '{{ json . }}'")
152
+ data = JSON.parse(cmd.stdout) if cmd.exit_status == 0
153
+ @version = Hashie::Mash.new(data)
154
+ rescue JSON::ParserError => _e
155
+ Hashie::Mash.new({})
156
+ end
157
+
158
+ def info
159
+ return @info if defined?(@info)
160
+
161
+ data = {}
162
+ # docke info format is only supported for Docker 17.03+
163
+ cmd = inspec.command("docker info --format '{{ json . }}'")
164
+ data = JSON.parse(cmd.stdout) if cmd.exit_status == 0
165
+ @info = Hashie::Mash.new(data)
166
+ rescue JSON::ParserError => _e
167
+ Hashie::Mash.new({})
168
+ end
169
+
170
+ # returns information about docker objects
171
+ def object(id)
172
+ return @inspect if defined?(@inspect)
173
+
174
+ data = JSON.parse(inspec.command("docker inspect #{id}").stdout)
175
+ data = data[0] if data.is_a?(Array)
176
+ @inspect = Hashie::Mash.new(data)
177
+ rescue JSON::ParserError => _e
178
+ Hashie::Mash.new({})
179
+ end
180
+
181
+ def to_s
182
+ "Docker Host"
183
+ end
184
+
185
+ private
186
+
187
+ def parse_json_command(labels, subcommand)
188
+ # build command
189
+ format = labels.map { |label| "\"#{label}\": {{json .#{label}}}" }
190
+ raw = inspec.command("docker #{subcommand} --format '{#{format.join(", ")}}'").stdout
191
+ output = []
192
+ # since docker is not outputting valid json, we need to parse each row
193
+ raw.each_line do |entry|
194
+ # convert all keys to lower_case to work well with ruby and filter table
195
+ row = JSON.parse(entry).map do |key, value|
196
+ [key.downcase, value]
197
+ end.to_h
198
+
199
+ # ensure all keys are there
200
+ row = ensure_keys(row, labels)
201
+
202
+ # strip off any linked container names
203
+ # Depending on how it was linked, the actual container name may come before
204
+ # or after the link information, so we'll just look for the first name that
205
+ # does not include a slash since that is not a valid character in a container name
206
+ if row["names"]
207
+ row["names"] = row["names"].split(",").find { |c| !c.include?("/") }
208
+ end
209
+
210
+ # Split labels on ',' or set to empty array
211
+ # Allows for `docker.containers.where { labels.include?('app=redis') }`
212
+ row["labels"] = row.key?("labels") ? row["labels"].split(",") : []
213
+
214
+ output.push(row)
215
+ end
216
+
217
+ output
218
+ rescue JSON::ParserError => _e
219
+ warn "Could not parse `docker #{subcommand}` output"
220
+ []
221
+ end
222
+
223
+ def parse_containers
224
+ # @see https://github.com/moby/moby/issues/20625, works for docker 1.13+
225
+ # raw_containers = inspec.command('docker ps -a --no-trunc --format \'{{ json . }}\'').stdout
226
+ # therefore we stick with older approach
227
+ labels = %w{Command CreatedAt ID Image Labels Mounts Names Ports RunningFor Size Status}
228
+
229
+ # Networks LocalVolumes work with 1.13+ only
230
+ if !version.empty? && Gem::Version.new(version["Client"]["Version"]) >= Gem::Version.new("1.13")
231
+ labels.push("Networks")
232
+ labels.push("LocalVolumes")
233
+ end
234
+ parse_json_command(labels, "ps -a --no-trunc")
235
+ end
236
+
237
+ def parse_services
238
+ parse_json_command(%w{ID Name Mode Replicas Image Ports}, "service ls")
239
+ end
240
+
241
+ def ensure_keys(entry, labels)
242
+ labels.each do |key|
243
+ entry[key.downcase] = nil unless entry.key?(key.downcase)
244
+ end
245
+ entry
246
+ end
247
+
248
+ def parse_images
249
+ # docker does not support the `json .` function here, therefore we need to emulate that behavior.
250
+ raw_images = inspec.command('docker images -a --no-trunc --format \'{ "id": {{json .ID}}, "repository": {{json .Repository}}, "tag": {{json .Tag}}, "size": {{json .Size}}, "digest": {{json .Digest}}, "createdat": {{json .CreatedAt}}, "createdsize": {{json .CreatedSince}} }\'').stdout
251
+ c_images = []
252
+ raw_images.each_line do |entry|
253
+ c_images.push(JSON.parse(entry))
254
+ end
255
+ c_images
256
+ rescue JSON::ParserError => _e
257
+ warn "Could not parse `docker images` output"
258
+ []
259
+ end
260
+
261
+ def parse_plugins
262
+ plugins = inspec.command('docker plugin ls --format \'{"id": {{json .ID}}, "name": "{{ with split .Name ":"}}{{index . 0}}{{end}}", "version": "{{ with split .Name ":"}}{{index . 1}}{{end}}", "enabled": {{json .Enabled}} }\'').stdout
263
+ c_plugins = []
264
+ plugins.each_line do |entry|
265
+ c_plugins.push(JSON.parse(entry))
266
+ end
267
+ c_plugins
268
+ rescue JSON::ParserError => _e
269
+ warn "Could not parse `docker plugin ls` output"
270
+ []
271
+ end
272
+ end
@@ -0,0 +1,114 @@
1
+ #
2
+ # Copyright 2017, Christoph Hartmann
3
+
4
+ require "inspec-docker-resources/resources/docker"
5
+ require "inspec-docker-resources/resources/docker_object"
6
+
7
+ class DockerContainer < Inspec.resource(1)
8
+ include DockerObject
9
+
10
+ name "docker_container"
11
+ supports platform: "unix"
12
+ desc ""
13
+ example <<~EXAMPLE
14
+ describe docker_container('an-echo-server') do
15
+ it { should exist }
16
+ it { should be_running }
17
+ its('id') { should_not eq '' }
18
+ its('image') { should eq 'busybox:latest' }
19
+ its('repo') { should eq 'busybox' }
20
+ its('tag') { should eq 'latest' }
21
+ its('ports') { should eq [] }
22
+ its('command') { should eq 'nc -ll -p 1234 -e /bin/cat' }
23
+ its('labels') { should include 'app=example' }
24
+ end
25
+
26
+ describe docker_container(id: 'e2c52a183358') do
27
+ it { should exist }
28
+ it { should be_running }
29
+ end
30
+ EXAMPLE
31
+
32
+ def initialize(opts = {})
33
+ # if a string is provided, we expect it is the name
34
+ if opts.is_a?(String)
35
+ @opts = { name: opts }
36
+ else
37
+ @opts = opts
38
+ end
39
+ end
40
+
41
+ def running?
42
+ status.downcase.start_with?("up") if object_info.entries.length == 1
43
+ end
44
+
45
+ # has_volume? matcher checks if the volume specified in source path of host is mounted in destination path of docker
46
+ def has_volume?(destination, source)
47
+ # volume_info is the hash which contains the low-level information about the container
48
+ # if Mounts key is not present or is nil; raise exception
49
+ raise Inspec::Exceptions::ResourceFailed, "Could not find any mounted volumes for your container" unless volume_info.Mounts[0]
50
+
51
+ # Iterate through the list of mounted volumes and check if it matches with the given destination and source
52
+ # is_mounted flag is used to handle to return explict boolean values of true or false
53
+ is_mounted = false
54
+ volume_info.Mounts.detect { |mount| is_mounted = mount.Destination == destination && mount.Source == source }
55
+ is_mounted
56
+ end
57
+
58
+ def status
59
+ object_info.status[0] if object_info.entries.length == 1
60
+ end
61
+
62
+ def labels
63
+ object_info.labels
64
+ end
65
+
66
+ def ports
67
+ object_info.ports[0] if object_info.entries.length == 1
68
+ end
69
+
70
+ def command
71
+ return unless object_info.entries.length == 1
72
+
73
+ cmd = object_info.commands[0]
74
+ cmd.slice(1, cmd.length - 2)
75
+ end
76
+
77
+ def image
78
+ object_info.images[0] if object_info.entries.length == 1
79
+ end
80
+
81
+ def repo
82
+ parse_components_from_image(image)[:repo] if object_info.entries.size == 1
83
+ end
84
+
85
+ def tag
86
+ parse_components_from_image(image)[:tag] if object_info.entries.size == 1
87
+ end
88
+
89
+ def to_s
90
+ name = @opts[:name] || @opts[:id]
91
+ "Docker Container #{name}"
92
+ end
93
+
94
+ def resource_id
95
+ object_info.ids[0] || @opts[:id] || @opts[:name] || ""
96
+ end
97
+
98
+ private
99
+
100
+ def object_info
101
+ return @info if defined?(@info)
102
+
103
+ opts = @opts
104
+ @info = inspec.docker.containers.where { names == opts[:name] || (!id.nil? && !opts[:id].nil? && (id == opts[:id] || id.start_with?(opts[:id]))) }
105
+ end
106
+
107
+ # volume_info returns the low-level information obtained on docker inspect [container_name/id]
108
+ def volume_info
109
+ return @mount_info if defined?(@mount_info)
110
+
111
+ # Check for either docker inspect [container_name] or docker inspect [container_id]
112
+ @mount_info = inspec.docker.object(@opts[:name] || @opts[:id])
113
+ end
114
+ end
@@ -0,0 +1,139 @@
1
+ #
2
+ # Copyright 2017, Christoph Hartmann
3
+
4
+ require "inspec-docker-resources/resources/docker"
5
+ require "inspec-docker-resources/resources/docker_object"
6
+
7
+ class DockerImage < Inspec.resource(1)
8
+ include DockerObject
9
+
10
+ name "docker_image"
11
+ supports platform: "unix"
12
+ desc ""
13
+ example <<~EXAMPLE
14
+ describe docker_image('alpine:latest') do
15
+ it { should exist }
16
+ its('id') { should_not eq '' }
17
+ its('image') { should eq 'alpine:latest' }
18
+ its('repo') { should eq 'alpine' }
19
+ its('tag') { should eq 'latest' }
20
+ end
21
+
22
+ describe docker_image('alpine:latest') do
23
+ it { should exist }
24
+ end
25
+
26
+ describe docker_image(id: '4a415e366388') do
27
+ it { should exist }
28
+ end
29
+ EXAMPLE
30
+
31
+ def initialize(opts = {})
32
+ # do sanitizion of input values
33
+ o = opts.dup
34
+ o = { image: opts } if opts.is_a?(String)
35
+ @opts = sanitize_options(o)
36
+ end
37
+
38
+ def image
39
+ "#{repo}:#{tag}" if object_info.entries.size == 1
40
+ end
41
+
42
+ def repo
43
+ object_info.repositories[0] if object_info.entries.size == 1
44
+ end
45
+
46
+ def tag
47
+ object_info.tags[0] if object_info.entries.size == 1
48
+ end
49
+
50
+ # method_missing handles when hash_keys are invoked to check information obtained on docker inspect [image_name]
51
+ def method_missing(*hash_keys)
52
+ # User can test the low-level inspect information in three ways:
53
+ # Way 1: Serverspec style: its(['Config.Cmd']) { should include some_value }
54
+ # here, the value for hash_keys recieved is [:[], "Config.Cmd"]
55
+ # Way 2: InSpec style: its(['Config','Cmd']) { should include some_value }
56
+ # here, the value for hash_keys recieved is [:[], "Config", "Cmd"]
57
+ # Way 3: Mix of both: its(['GraphDriver.Data','MergedDir']) { should include some_value }
58
+ # here, the value for hash_keys recieved is [:[], "GraphDriver.Data", "MergedDir"]
59
+
60
+ # hash_keys are passed to this method to evaluate the value
61
+ image_hash_inspection(hash_keys)
62
+ end
63
+
64
+ # inspection property allows to test any of the hash key-value pairs as part of the image_inspect_info
65
+ def inspection
66
+ image_inspect_info
67
+ end
68
+
69
+ def to_s
70
+ img = @opts[:image] || @opts[:id]
71
+ "Docker Image #{img}"
72
+ end
73
+
74
+ def resource_id
75
+ object_info.ids[0] || @opts[:id] || @opts[:image] || ""
76
+ end
77
+
78
+ private
79
+
80
+ def sanitize_options(opts)
81
+ opts.merge!(parse_components_from_image(opts[:image]))
82
+
83
+ # assume a "latest" tag if we don't have one
84
+ opts[:tag] ||= "latest"
85
+
86
+ # if the ID isn't nil and doesn't contain a hash indicator (indicated by the presence
87
+ # of a colon, which separates the indicator from the actual hash), we assume it's sha256.
88
+ opts[:id] = "sha256:" + opts[:id] unless opts[:id].nil? || opts[:id].include?(":")
89
+
90
+ # Assemble/reassemble the image from the repo and tag
91
+ opts[:image] = "#{opts[:repo]}:#{opts[:tag]}" unless opts[:repo].nil?
92
+
93
+ # return the santized opts back to the caller
94
+ opts
95
+ end
96
+
97
+ def object_info
98
+ return @info if defined?(@info)
99
+
100
+ opts = @opts
101
+ @info = inspec.docker.images.where do
102
+ (repository == opts[:repo] && tag == opts[:tag]) || (!id.nil? && !opts[:id].nil? && (id == opts[:id] || id.start_with?(opts[:id])))
103
+ end
104
+ end
105
+
106
+ # image_inspect_info returns the complete inspect hash_values of the image
107
+ def image_inspect_info
108
+ return @inspect_info if defined?(@inspect_info)
109
+
110
+ @inspect_info = inspec.docker.object(@opts[:image] || (!@opts[:id].nil? && @opts[:id]))
111
+ end
112
+
113
+ # image_hash_inspection formats the input hash_keys and checks if any value exists for such keys in @inspect_info(image_inspect_info)
114
+ def image_hash_inspection(hash_keys)
115
+ # The hash_keys recieved are in three formats as mentioned in method_missing
116
+ # The hash_keys recieved must be in array format [] and the zeroth index must be :[]
117
+ # Check for the conditions and remove the zeroth element from the hash_keys
118
+
119
+ hash_keys.shift if hash_keys.is_a?(Array) && hash_keys[0] == :[]
120
+
121
+ # When received hash_keys in Serverspec style or mix of both
122
+ # The hash_keys are to be splitted at '.' (dot) and flatten it so that it doesn't become array of arrays
123
+ # After splitting and flattening is done, hash_keys is now an array with individual keys
124
+ hash_keys = hash_keys.map { |key| key.split(".") }.flatten
125
+
126
+ # image_inspect_info returns the complete inspect hash_values of the image
127
+ # dig() finds the nested value specified by the sequence of the key object by calling dig at each step.
128
+ # hash_keys is the key object. If one of the key is bad, value will be nil.
129
+ hash_value = image_inspect_info.dig(*hash_keys)
130
+
131
+ # If one of the key is bad, hash_value will be nil, so raise exception which throws it in rescue block
132
+ # else return hash_value
133
+ raise Inspec::Exceptions::ResourceFailed if hash_value.nil?
134
+
135
+ hash_value
136
+ rescue
137
+ raise Inspec::Exceptions::ResourceFailed, "#{hash_keys.join(".")} is not a valid key for your image or has nil value."
138
+ end
139
+ end
@@ -0,0 +1,52 @@
1
+ #
2
+ # Copyright 2017, Christoph Hartmann
3
+ #
4
+
5
+ module DockerObject
6
+ def exist?
7
+ object_info.exists?
8
+ end
9
+
10
+ def id
11
+ object_info.ids[0] if object_info.entries.size == 1
12
+ end
13
+
14
+ private
15
+
16
+ def parse_components_from_image(image_string)
17
+ # if the user did not supply an image string, they likely supplied individual
18
+ # option parameters, such as repo and tag. Return empty data back to the caller.
19
+ return {} if image_string.nil?
20
+
21
+ first_colon = image_string.index(":") || -1
22
+ first_slash = image_string.index("/") || -1
23
+
24
+ if image_string.count(":") == 2
25
+ # If there are two colons in the image string, it contains a repo-with-port and a tag.
26
+ # example: localhost:5000/chef/inspec:1.46.3
27
+ partitioned_string = image_string.rpartition(":")
28
+ repo = partitioned_string.first
29
+ tag = partitioned_string.last
30
+ image_name = repo.split("/")[1..-1].join
31
+ elsif image_string.count(":") == 1 && first_colon < first_slash
32
+ # If there's one colon in the image string, and it comes before a forward-slash,
33
+ # it contains a repo-with-port but no tag.
34
+ # example: localhost:5000/ubuntu
35
+ repo = image_string
36
+ tag = nil
37
+ image_name = repo.split("/")[1..-1].join
38
+ else
39
+ # If there's one colon in the image string and it doesn't preceed a slash, or if
40
+ # there is no colon at all, then it separates the repo from the tag, if there is a tag.
41
+ # example: chef/inspec:1.46.3
42
+ # example: chef/inspec
43
+ # example: ubuntu:14.04
44
+ repo, tag = image_string.split(":")
45
+ image_name = repo
46
+ end
47
+
48
+ # return the repo, image_name and tag parsed from the string, which can be merged into
49
+ # the rest of the user-supplied options
50
+ { repo: repo, image_name: image_name, tag: tag }
51
+ end
52
+ end
@@ -0,0 +1,66 @@
1
+ require "inspec-docker-resources/resources/docker"
2
+
3
+ class DockerPlugin < Inspec.resource(1)
4
+ name "docker_plugin"
5
+ supports platform: "unix"
6
+ desc "Retrieves info about docker plugins"
7
+ example <<~EXAMPLE
8
+ describe docker_plugin('rexray/ebs') do
9
+ it { should exist }
10
+ its('id') { should_not eq '0ac30b93ad40' }
11
+ its('version') { should eq '0.11.1' }
12
+ it { should be_enabled }
13
+ end
14
+
15
+ describe docker_plugin('alpine:latest') do
16
+ it { should exist }
17
+ end
18
+
19
+ describe docker_plugin(id: '4a415e366388') do
20
+ it { should exist }
21
+ end
22
+ EXAMPLE
23
+
24
+ def initialize(opts = {})
25
+ # do sanitizion of input values
26
+ o = opts.dup
27
+ o = { name: opts } if opts.is_a?(String)
28
+ @opts = o
29
+ end
30
+
31
+ def exist?
32
+ object_info.entries.size == 1
33
+ end
34
+
35
+ def enabled?
36
+ object_info.enabled[0]
37
+ end
38
+
39
+ def id
40
+ object_info.ids[0] if object_info.entries.size == 1
41
+ end
42
+
43
+ def version
44
+ object_info.versions[0] if object_info.entries.size == 1
45
+ end
46
+
47
+ def to_s
48
+ plugin = @opts[:name] || @opts[:id]
49
+ "Docker plugin #{plugin}"
50
+ end
51
+
52
+ def resource_id
53
+ id || @opts[:id] || @opts[:name] || ""
54
+ end
55
+
56
+ private
57
+
58
+ def object_info
59
+ return @info if defined?(@info)
60
+
61
+ opts = @opts
62
+ @info = inspec.docker.plugins.where do
63
+ (name == opts[:name]) || (!id.nil? && !opts[:id].nil? && (id == opts[:id]))
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,93 @@
1
+ #
2
+ # Copyright 2017, Christoph Hartmann
3
+
4
+ require "inspec-docker-resources/resources/docker"
5
+ require "inspec-docker-resources/resources/docker_object"
6
+
7
+ class DockerService < Inspec.resource(1)
8
+ include DockerObject
9
+
10
+ name "docker_service"
11
+ supports platform: "unix"
12
+ desc "Swarm-mode service"
13
+ example <<~EXAMPLE
14
+ describe docker_service('service1') do
15
+ it { should exist }
16
+ its('id') { should_not eq '' }
17
+ its('image') { should eq 'alpine:latest' }
18
+ its('repo') { should eq 'alpine' }
19
+ its('tag') { should eq 'latest' }
20
+ end
21
+
22
+ describe docker_service(id: '4a415e366388') do
23
+ it { should exist }
24
+ end
25
+
26
+ describe docker_service(image: 'alpine:latest') do
27
+ it { should exist }
28
+ end
29
+ EXAMPLE
30
+
31
+ def initialize(opts = {})
32
+ # do sanitizion of input values
33
+ o = opts.dup
34
+ o = { name: opts } if opts.is_a?(String)
35
+ @opts = sanitize_options(o)
36
+ end
37
+
38
+ def name
39
+ object_info.names[0] if object_info.entries.size == 1
40
+ end
41
+
42
+ def image
43
+ object_info.images[0] if object_info.entries.size == 1
44
+ end
45
+
46
+ def image_name
47
+ parse_components_from_image(image)[:image_name] if object_info.entries.size == 1
48
+ end
49
+
50
+ def repo
51
+ parse_components_from_image(image)[:repo] if object_info.entries.size == 1
52
+ end
53
+
54
+ def tag
55
+ parse_components_from_image(image)[:tag] if object_info.entries.size == 1
56
+ end
57
+
58
+ def mode
59
+ object_info.modes[0] if object_info.entries.size == 1
60
+ end
61
+
62
+ def replicas
63
+ object_info.replicas[0] if object_info.entries.size == 1
64
+ end
65
+
66
+ def ports
67
+ object_info.ports[0] if object_info.entries.size == 1
68
+ end
69
+
70
+ def to_s
71
+ service = @opts[:name] || @opts[:id]
72
+ "Docker Service #{service}"
73
+ end
74
+
75
+ def resource_id
76
+ object_info.ids[0] || @opts[:id] || @opts[:name] || ""
77
+ end
78
+
79
+ private
80
+
81
+ def sanitize_options(opts)
82
+ opts.merge(parse_components_from_image(opts[:image]))
83
+ end
84
+
85
+ def object_info
86
+ return @info if defined?(@info)
87
+
88
+ opts = @opts
89
+ @info = inspec.docker.services.where do
90
+ name == opts[:name] || image == opts[:image] || (!id.nil? && !opts[:id].nil? && (id == opts[:id] || id.start_with?(opts[:id])))
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ # This file simply makes it easier for CI engines to update
3
+ # the version stamp, and provide a clean way for the gemspec
4
+ # to learn the current version.
5
+ module InspecPlugins
6
+ module DockerResources
7
+ VERSION = "7.1.5"
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # This file is known as the "entry point."
2
+ # This is the file InSpec will try to load if it
3
+ # thinks your plugin is installed.
4
+
5
+ # The *only* thing this file should do is setup the
6
+ # load path, then load the plugin definition file.
7
+
8
+ # Next two lines simply add the path of the gem to the load path.
9
+ # This is not needed when being loaded as a gem; but when doing
10
+ # plugin development, you may need it. Either way, it's harmless.
11
+ libdir = File.dirname(__FILE__)
12
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
13
+
14
+ require "inspec-docker-resources/plugin"
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: inspec-docker-resources
3
+ version: !ruby/object:Gem::Version
4
+ version: 7.1.5
5
+ platform: ruby
6
+ authors:
7
+ - InSpec Core Maintainers
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-10-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: inspec-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ description: Contains InSpec 7.0+ resources fo interacting with Docker Desktop.
28
+ email:
29
+ - inspec@progress.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - README.md
36
+ - inspec-docker-resources.gemspec
37
+ - inspec.yml
38
+ - lib/inspec-docker-resources.rb
39
+ - lib/inspec-docker-resources/plugin.rb
40
+ - lib/inspec-docker-resources/resource_pack.rb
41
+ - lib/inspec-docker-resources/resources/.gitkeep
42
+ - lib/inspec-docker-resources/resources/docker.rb
43
+ - lib/inspec-docker-resources/resources/docker_container.rb
44
+ - lib/inspec-docker-resources/resources/docker_image.rb
45
+ - lib/inspec-docker-resources/resources/docker_object.rb
46
+ - lib/inspec-docker-resources/resources/docker_plugin.rb
47
+ - lib/inspec-docker-resources/resources/docker_service.rb
48
+ - lib/inspec-docker-resources/version.rb
49
+ homepage: https://github.com/inspec/inspec-docker-resources
50
+ licenses:
51
+ - Apache-2.0
52
+ metadata: {}
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.1.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.3.27
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: Docker InSpec Resources in a Gem
72
+ test_files: []