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

new docker_service resource to inspect Docker Swarm services #2456

Merged
merged 1 commit into from
Jan 23, 2018
Merged
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
107 changes: 107 additions & 0 deletions docs/resources/docker_service.md.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: About the docker_service Resource
---

# docker_service

Use the `docker_service` InSpec audit resource to verify a docker swarm service.

<br>

## Syntax

A `docker_service` resource block declares the service by name:

describe docker_service('foo') do
it { should exist }
its('id') { should eq '2ghswegspre1' }
its('repo') { should eq 'alpine' }
its('tag') { should eq 'latest' }
end

The resource allows you to pass in a service id:

describe docker_service(id: '2ghswegspre1') do
...
end

You can also pass in the fully-qualified image:

describe docker_service(image: 'localhost:5000/alpine:latest') do
...
end

<br>

## Examples

The following examples show how to use this InSpec `docker_service` resource.

### Test a docker service

describe docker_service('foo') do
it { should exist }
its('id') { should eq '2ghswegspre1' }
its('repo') { should eq 'alpine' }
its('tag') { should eq 'latest' }
end

<br>

## Matchers

This InSpec audit resource has the following matchers. For a full list of available matchers please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).

### exist

The `exist` matcher tests if the image is available on the node:

it { should exist }

### id

The `id` matcher returns the service id:

its('id') { should eq '2ghswegspre1' }

### image

The `image` matcher tests the value of the image. It is a combination of `repository:tag`:

its('image') { should eq 'alpine:latest' }

### mode

The `mode` matcher tests the value of the service mode:

its('mode') { should eq 'replicated' }

### name

The `name` matcher tests the value of the service name:

its('name') { should eq 'foo' }

### ports

The `ports` matcher tests the value of the service's published ports:

its('ports') { should include '*:8000->8000/tcp' }

### repo

The `repo` matcher tests the value of the repository name:

its('repo') { should eq 'alpine' }

### replicas

The `replicas` matcher tests the value of the service's replica count:

its('replicas') { should eq '3/3' }

### tag

The `tag` matcher tests the value of image tag:

its('tag') { should eq 'latest' }
1 change: 1 addition & 0 deletions lib/inspec/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def self.validate_resource_dsl_version!(version)
require 'resources/docker'
require 'resources/docker_container'
require 'resources/docker_image'
require 'resources/docker_service'
require 'resources/elasticsearch'
require 'resources/etc_fstab'
require 'resources/etc_group'
Expand Down
80 changes: 57 additions & 23 deletions lib/resources/docker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ def initialize(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
Expand All @@ -79,6 +98,10 @@ class Docker < Inspec.resource(1)
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'}
Expand All @@ -105,6 +128,10 @@ def images
DockerImageFilter.new(parse_images)
end

def services
DockerServiceFilter.new(parse_services)
end

def version
return @version if defined?(@version)
data = {}
Expand Down Expand Up @@ -142,48 +169,55 @@ def to_s

private

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
def parse_json_command(labels, subcommand)
# build command
format = labels.map { |label| "\"#{label}\": {{json .#{label}}}" }
raw_containers = inspec.command("docker ps -a --no-trunc --format '{#{format.join(', ')}}'").stdout
ps = []
raw = inspec.command("docker #{subcommand} --format '{#{format.join(', ')}}'").stdout
output = []
# since docker is not outputting valid json, we need to parse each row
raw_containers.each_line { |entry|
j = JSON.parse(entry)
raw.each_line { |entry|
# convert all keys to lower_case to work well with ruby and filter table
j = j.map { |k, v|
j = JSON.parse(entry).map { |k, v|
[k.downcase, v]
}.to_h

# ensure all keys are there
j = ensure_container_keys(j)
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?('/') }
j['names'] = j['names'].split(',').find { |c| !c.include?('/') } if j.key?('names')

ps.push(j)
output.push(j)
}
ps
output
rescue JSON::ParserError => _e
warn 'Could not parse `docker ps` output'
warn "Could not parse `docker #{subcommand}` output"
[]
end

def ensure_container_keys(entry)
%w{Command CreatedAt ID Image Labels Mounts Names Ports RunningFor Size Status Networks LocalVolumes}.each { |key|
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
Expand Down
50 changes: 14 additions & 36 deletions lib/resources/docker_container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
# author: Patrick Muench
# author: Dominik Richter

require_relative 'docker_object'

module Inspec::Resources
class DockerContainer < Inspec.resource(1)
include Inspec::Resources::DockerObject

name 'docker_container'
desc ''
example "
Expand Down Expand Up @@ -37,55 +41,39 @@ def initialize(opts = {})
end
end

def exist?
container_info.exists?
end

# is allways returning the full id
def id
container_info.ids[0] if container_info.entries.length == 1
end

def running?
status.downcase.start_with?('up') if container_info.entries.length == 1
status.downcase.start_with?('up') if object_info.entries.length == 1
end

def status
container_info.status[0] if container_info.entries.length == 1
object_info.status[0] if object_info.entries.length == 1
end

def labels
container_info.labels[0] if container_info.entries.length == 1
object_info.labels[0] if object_info.entries.length == 1
end

def ports
container_info.ports[0] if container_info.entries.length == 1
object_info.ports[0] if object_info.entries.length == 1
end

def command
return unless container_info.entries.length == 1
return unless object_info.entries.length == 1

cmd = container_info.commands[0]
cmd = object_info.commands[0]
cmd.slice(1, cmd.length - 2)
end

def image
container_info.images[0] if container_info.entries.length == 1
object_info.images[0] if object_info.entries.length == 1
end

def repo
return if image.nil? || image_name_from_image.nil?
if image.include?('/') # host:port/ubuntu:latest
repo_part, image_part = image.split('/') # host:port, ubuntu:latest
repo_part + '/' + image_part.split(':')[0] # host:port + / + ubuntu
else
image_name_from_image.split(':')[0]
end
parse_components_from_image(image)[:repo] if object_info.entries.size == 1
end

def tag
return if image_name_from_image.nil?
image_name_from_image.split(':')[1]
parse_components_from_image(image)[:tag] if object_info.entries.size == 1
end

def to_s
Expand All @@ -95,17 +83,7 @@ def to_s

private

def image_name_from_image
return if image.nil?
# possible image names include:
# alpine
# ubuntu:14.04
# repo.example.com:5000/ubuntu
# repo.example.com:5000/ubuntu:1404
image.include?('/') ? image.split('/')[1] : image
end

def container_info
def object_info
return @info if defined?(@info)
opts = @opts
@info = inspec.docker.containers.where { names == opts[:name] || (!id.nil? && !opts[:id].nil? && (id == opts[:id] || id.start_with?(opts[:id]))) }
Expand Down
Loading