2017-04-24 14:47:03 +00:00
|
|
|
# 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
|
|
|
|
|
2018-01-23 20:30:14 +00:00
|
|
|
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
|
|
|
|
|
2017-04-24 14:47:03 +00:00
|
|
|
# This resource helps to parse information from the docker host
|
|
|
|
# For compatability with Serverspec we also offer the following resouses:
|
|
|
|
# - docker_container
|
|
|
|
# - docker_image
|
2017-12-07 19:22:55 +00:00
|
|
|
class Docker < Inspec.resource(1)
|
2017-04-24 14:47:03 +00:00
|
|
|
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
|
|
|
|
|
2018-01-23 20:30:14 +00:00
|
|
|
describe docker.services do
|
|
|
|
its('images') { should_not include 'inssecure_image' }
|
|
|
|
end
|
|
|
|
|
2017-04-24 14:47:03 +00:00
|
|
|
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
|
|
|
|
|
2018-01-23 20:30:14 +00:00
|
|
|
def services
|
|
|
|
DockerServiceFilter.new(parse_services)
|
|
|
|
end
|
|
|
|
|
2017-04-24 14:47:03 +00:00
|
|
|
def version
|
|
|
|
return @version if defined?(@version)
|
2017-06-26 19:45:03 +00:00
|
|
|
data = {}
|
|
|
|
cmd = inspec.command('docker version --format \'{{ json . }}\'')
|
|
|
|
data = JSON.parse(cmd.stdout) if cmd.exit_status == 0
|
2017-04-24 14:47:03 +00:00
|
|
|
@version = Hashie::Mash.new(data)
|
2017-04-25 09:45:36 +00:00
|
|
|
rescue JSON::ParserError => _e
|
|
|
|
return Hashie::Mash.new({})
|
2017-04-24 14:47:03 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def info
|
|
|
|
return @info if defined?(@info)
|
2017-06-26 19:45:03 +00:00
|
|
|
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
|
2017-04-24 14:47:03 +00:00
|
|
|
@info = Hashie::Mash.new(data)
|
2017-04-25 09:45:36 +00:00
|
|
|
rescue JSON::ParserError => _e
|
|
|
|
return Hashie::Mash.new({})
|
2017-04-24 14:47:03 +00:00
|
|
|
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)
|
2017-04-25 09:45:36 +00:00
|
|
|
rescue JSON::ParserError => _e
|
|
|
|
return Hashie::Mash.new({})
|
2017-04-24 14:47:03 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def to_s
|
|
|
|
'Docker Host'
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2018-01-23 20:30:14 +00:00
|
|
|
def parse_json_command(labels, subcommand)
|
2017-06-26 19:45:03 +00:00
|
|
|
# build command
|
|
|
|
format = labels.map { |label| "\"#{label}\": {{json .#{label}}}" }
|
2018-01-23 20:30:14 +00:00
|
|
|
raw = inspec.command("docker #{subcommand} --format '{#{format.join(', ')}}'").stdout
|
|
|
|
output = []
|
2017-04-24 14:47:03 +00:00
|
|
|
# since docker is not outputting valid json, we need to parse each row
|
2018-01-23 20:30:14 +00:00
|
|
|
raw.each_line { |entry|
|
2017-04-24 14:47:03 +00:00
|
|
|
# convert all keys to lower_case to work well with ruby and filter table
|
2018-01-23 20:30:14 +00:00
|
|
|
j = JSON.parse(entry).map { |k, v|
|
2017-04-24 14:47:03 +00:00
|
|
|
[k.downcase, v]
|
|
|
|
}.to_h
|
2017-06-26 19:45:03 +00:00
|
|
|
|
|
|
|
# ensure all keys are there
|
2018-01-23 20:30:14 +00:00
|
|
|
j = ensure_keys(j, labels)
|
2017-09-13 12:16:53 +00:00
|
|
|
|
|
|
|
# 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
|
2018-01-23 20:30:14 +00:00
|
|
|
j['names'] = j['names'].split(',').find { |c| !c.include?('/') } if j.key?('names')
|
2017-09-13 12:16:53 +00:00
|
|
|
|
2018-01-23 20:30:14 +00:00
|
|
|
output.push(j)
|
2017-04-24 14:47:03 +00:00
|
|
|
}
|
2018-01-23 20:30:14 +00:00
|
|
|
output
|
2017-04-25 09:45:36 +00:00
|
|
|
rescue JSON::ParserError => _e
|
2018-01-23 20:30:14 +00:00
|
|
|
warn "Could not parse `docker #{subcommand}` output"
|
2017-04-25 09:45:36 +00:00
|
|
|
[]
|
2017-04-24 14:47:03 +00:00
|
|
|
end
|
|
|
|
|
2018-01-23 20:30:14 +00:00
|
|
|
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|
|
2017-06-26 19:45:03 +00:00
|
|
|
entry[key.downcase] = nil if !entry.key?(key.downcase)
|
|
|
|
}
|
|
|
|
entry
|
|
|
|
end
|
|
|
|
|
2017-04-24 14:47:03 +00:00
|
|
|
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
|
2017-04-25 09:45:36 +00:00
|
|
|
rescue JSON::ParserError => _e
|
|
|
|
warn 'Could not parse `docker images` output'
|
|
|
|
[]
|
2017-04-24 14:47:03 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|