inspec/lib/resources/docker.rb
Matt Kulka c067798fc5 Docker Swarm service resource (#2456)
This change adds the `docker_service` resource for Docker swarm mode services. This
branches off some of the common elements (id, exists) into a `DockerObject` module along
with a utility function for parsing the image/repo string. That function was implemented
separately by `docker_image` and `docker_container`, now with a third resource, it made
sense to consolidate that into an included module. I used the most comprehensive
implementation. Existing classes had to be slightly modified for the genericization.

Signed-off-by: Matt Kulka <mkulka@parchment.com>
2018-01-23 12:30:14 -08:00

239 lines
7.6 KiB
Ruby

# encoding: utf-8
#
# Copyright 2017, Christoph Hartmann
#
# author: Christoph Hartmann
# author: Patrick Muench
# author: Dominik Richter
require 'utils/filter'
require 'hashie/mash'
module Inspec::Resources
class DockerContainerFilter
# use filtertable for containers
filter = FilterTable.create
filter.add_accessor(:where)
.add_accessor(:entries)
.add(:commands, field: 'command')
.add(:ids, field: 'id')
.add(:images, field: 'image')
.add(:labels, field: 'labels')
.add(:local_volumes, field: 'localvolumes')
.add(:mounts, field: 'mounts')
.add(:names, field: 'names')
.add(:networks, field: 'networks')
.add(:ports, field: 'ports')
.add(:running_for, field: 'runningfor')
.add(:sizes, field: 'size')
.add(:status, field: 'status')
.add(:exists?) { |x| !x.entries.empty? }
.add(:running?) { |x|
x.where { status.downcase.start_with?('up') }
}
filter.connect(self, :containers)
attr_reader :containers
def initialize(containers)
@containers = containers
end
end
class DockerImageFilter
filter = FilterTable.create
filter.add_accessor(:where)
.add_accessor(:entries)
.add(:ids, field: 'id')
.add(:repositories, field: 'repository')
.add(:tags, field: 'tag')
.add(:sizes, field: 'size')
.add(:digests, field: 'digest')
.add(:created, field: 'createdat')
.add(:created_since, field: 'createdsize')
.add(:exists?) { |x| !x.entries.empty? }
filter.connect(self, :images)
attr_reader :images
def initialize(images)
@images = images
end
end
class DockerServiceFilter
filter = FilterTable.create
filter.add_accessor(:where)
.add_accessor(:entries)
.add(:ids, field: 'id')
.add(:names, field: 'name')
.add(:modes, field: 'mode')
.add(:replicas, field: 'replicas')
.add(:images, field: 'image')
.add(:ports, field: 'ports')
.add(:exists?) { |x| !x.entries.empty? }
filter.connect(self, :services)
attr_reader :services
def initialize(services)
@services = services
end
end
# This resource helps to parse information from the docker host
# For compatability with Serverspec we also offer the following resouses:
# - docker_container
# - docker_image
class Docker < Inspec.resource(1)
name 'docker'
desc "
A resource to retrieve information about docker
"
example "
describe docker.containers do
its('images') { should_not include 'u12:latest' }
end
describe docker.images do
its('repositories') { should_not include 'inssecure_image' }
end
describe docker.services do
its('images') { should_not include 'inssecure_image' }
end
describe docker.version do
its('Server.Version') { should cmp >= '1.12'}
its('Client.Version') { should cmp >= '1.12'}
end
describe docker.object(id) do
its('Configuration.Path') { should eq 'value' }
end
docker.containers.ids.each do |id|
# call docker inspect for a specific container id
describe docker.object(id) do
its(%w(HostConfig Privileged)) { should cmp false }
its(%w(HostConfig Privileged)) { should_not cmp true }
end
end
"
def containers
DockerContainerFilter.new(parse_containers)
end
def images
DockerImageFilter.new(parse_images)
end
def services
DockerServiceFilter.new(parse_services)
end
def version
return @version if defined?(@version)
data = {}
cmd = inspec.command('docker version --format \'{{ json . }}\'')
data = JSON.parse(cmd.stdout) if cmd.exit_status == 0
@version = Hashie::Mash.new(data)
rescue JSON::ParserError => _e
return Hashie::Mash.new({})
end
def info
return @info if defined?(@info)
data = {}
# docke info format is only supported for Docker 17.03+
cmd = inspec.command('docker info --format \'{{ json . }}\'')
data = JSON.parse(cmd.stdout) if cmd.exit_status == 0
@info = Hashie::Mash.new(data)
rescue JSON::ParserError => _e
return Hashie::Mash.new({})
end
# returns information about docker objects
def object(id)
return @inspect if defined?(@inspect)
data = JSON.parse(inspec.command("docker inspect #{id}").stdout)
data = data[0] if data.is_a?(Array)
@inspect = Hashie::Mash.new(data)
rescue JSON::ParserError => _e
return Hashie::Mash.new({})
end
def to_s
'Docker Host'
end
private
def parse_json_command(labels, subcommand)
# build command
format = labels.map { |label| "\"#{label}\": {{json .#{label}}}" }
raw = inspec.command("docker #{subcommand} --format '{#{format.join(', ')}}'").stdout
output = []
# since docker is not outputting valid json, we need to parse each row
raw.each_line { |entry|
# convert all keys to lower_case to work well with ruby and filter table
j = JSON.parse(entry).map { |k, v|
[k.downcase, v]
}.to_h
# ensure all keys are there
j = ensure_keys(j, labels)
# strip off any linked container names
# Depending on how it was linked, the actual container name may come before
# or after the link information, so we'll just look for the first name that
# does not include a slash since that is not a valid character in a container name
j['names'] = j['names'].split(',').find { |c| !c.include?('/') } if j.key?('names')
output.push(j)
}
output
rescue JSON::ParserError => _e
warn "Could not parse `docker #{subcommand}` output"
[]
end
def parse_containers
# @see https://github.com/moby/moby/issues/20625, works for docker 1.13+
# raw_containers = inspec.command('docker ps -a --no-trunc --format \'{{ json . }}\'').stdout
# therefore we stick with older approach
labels = %w{Command CreatedAt ID Image Labels Mounts Names Ports RunningFor Size Status}
# Networks LocalVolumes work with 1.13+ only
if !version.empty? && Gem::Version.new(version['Client']['Version']) >= Gem::Version.new('1.13')
labels.push('Networks')
labels.push('LocalVolumes')
end
parse_json_command(labels, 'ps -a --no-trunc')
end
def parse_services
parse_json_command(%w{ID Name Mode Replicas Image Ports}, 'service ls')
end
def ensure_keys(entry, labels)
labels.each { |key|
entry[key.downcase] = nil if !entry.key?(key.downcase)
}
entry
end
def parse_images
# docker does not support the `json .` function here, therefore we need to emulate that behavior.
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
c_images = []
raw_images.each_line { |entry|
c_images.push(JSON.parse(entry))
}
c_images
rescue JSON::ParserError => _e
warn 'Could not parse `docker images` output'
[]
end
end
end