Skip to content

Commit

Permalink
Merge pull request #1593 from zendesk/grosser/clair
Browse files Browse the repository at this point in the history
Covert clair support to a plugin
  • Loading branch information
grosser authored Dec 30, 2016
2 parents 2b30486 + b0114e0 commit 459a5d5
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 202 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,6 @@ GITHUB_TOKEN=
# JENKINS_URL= # server_url of jenkins
# JENKINS_USERNAME= # user id
# JENKINS_API_KEY= # API Token from user / Configure page

## Hyperclair, optional to security scan built docker images
# HYPERCLAIR_PATH=/usr/local/bin/hyperclair
6 changes: 6 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ PATH
samson_flowdock (0.0.0)
flowdock (~> 0.6.0)

PATH
remote: plugins/hyperclair
specs:
samson_hyperclair (0.0.0)

PATH
remote: plugins/jenkins
specs:
Expand Down Expand Up @@ -579,6 +584,7 @@ DEPENDENCIES
samson_dockerb!
samson_env!
samson_flowdock!
samson_hyperclair!
samson_jenkins!
samson_kubernetes!
samson_ledger!
Expand Down
40 changes: 15 additions & 25 deletions app/models/docker_builder_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ def initialize(build)
@build = build
end

def run!(image_name: nil, push: false, tag_as_latest: false)
def run!(push: false, tag_as_latest: false)
build.docker_build_job.try(:destroy) # if there's an old build job, delete it
build.docker_ref = build.label.try(:parameterize).presence || 'latest'

job = build.create_docker_job
build.save!
Expand All @@ -55,21 +56,14 @@ def run!(image_name: nil, push: false, tag_as_latest: false)
@output = execution.output
repository.executor = execution.executor

success =
if build.kubernetes_job
run_build_image_job(job, image_name, push: push, tag_as_latest: tag_as_latest)
elsif build_image(tmp_dir)
ret = true
ret = push_image(image_name, tag_as_latest: tag_as_latest) if push
build.docker_image.remove(force: true) unless ENV["DOCKER_KEEP_BUILT_IMGS"] == "1"
ret
end

if success
Samson::Clair.append_job_with_scan(job, docker_image_ref(image_name, build))
if build.kubernetes_job
run_build_image_job(job, push: push, tag_as_latest: tag_as_latest)
elsif build_image(tmp_dir)
ret = true
ret = push_image(tag_as_latest: tag_as_latest) if push
build.docker_image.remove(force: true) unless ENV["DOCKER_KEEP_BUILT_IMGS"] == "1"
ret
end

success
end

job_execution.on_complete { send_after_notifications }
Expand Down Expand Up @@ -103,21 +97,21 @@ def run!(image_name: nil, push: false, tag_as_latest: false)
File.new(tempfile_name, 'r')
end

def run_build_image_job(local_job, image_name, push: false, tag_as_latest: false)
docker_ref = docker_image_ref(image_name, build)
# TODO: not calling before_docker_build hooks since we don't have a temp directory
# possibly call it anyway with nil so calls do not get lost
def run_build_image_job(local_job, push: false, tag_as_latest: false)
k8s_job = Kubernetes::BuildJobExecutor.new(
output,
job: local_job,
registry: self.class.registry_credentials(Rails.application.config.samson.docker.registries.first)
)
success, build_log = k8s_job.execute!(
build, project,
docker_ref: docker_ref,
docker_ref: build.docker_ref,
push: push,
tag_as_latest: tag_as_latest
)

build.docker_ref = docker_ref
build.docker_repo_digest = nil

if success
Expand Down Expand Up @@ -152,8 +146,8 @@ def build_image(tmp_dir)
end
add_method_tracer :build_image

def push_image(tag, tag_as_latest: false)
tag = build.docker_ref = docker_image_ref(tag, build)
def push_image(tag_as_latest: false)
tag = build.docker_ref
tag_is_latest = (tag == 'latest')

unless build.docker_repo_digest = push_image_to_registries(tag: tag, override_tag: tag_is_latest)
Expand Down Expand Up @@ -221,10 +215,6 @@ def project
@build.project
end

def docker_image_ref(image_name, build)
image_name.presence || build.label.try(:parameterize).presence || 'latest'
end

def send_after_notifications
Samson::Hooks.fire(:after_docker_build, build)
SseRailsEngine.send_event('builds', type: 'finish', build: BuildSerializer.new(build, root: nil))
Expand Down
1 change: 1 addition & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Available plugins:
- [Flowdock notification](https://github.com/zendesk/samson/tree/master/plugins/flowdock)
- [Jenkins jobs management](https://github.com/zendesk/samson/tree/master/plugins/jenkins)
- [Hipchat notification](https://github.com/listia/samson_hipchat)
- [Hyperclair security scanner for docker images](https://github.com/zendesk/samson/tree/master/plugins/hyperclair)
- [Kubernetes](https://github.com/zendesk/samson/tree/master/plugins/kubernetes)
- [Release Number From CI](https://github.com/redbubble/samson-release-number-from-ci)
- [NewRelic monitoring](https://github.com/zendesk/samson/tree/master/plugins/new_relic)
Expand Down
55 changes: 0 additions & 55 deletions lib/samson/clair.rb

This file was deleted.

6 changes: 6 additions & 0 deletions plugins/hyperclair/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Hyperclair will pull the image from registry and run scan with Clair scanner.
This is supposed to use a forked version with `ENV` var and `/` support.
https://github.com/zendesk/hyperclair
(discussion on why we use a fork / when we can switch see https://github.com/wemanity-belgium/hyperclair/pull/90)

Set `HYPERCLAIR_PATH` ENV variable in samson to enable.
61 changes: 61 additions & 0 deletions plugins/hyperclair/lib/samson_hyperclair/samson_plugin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true
# TODO: should check based on docker_repo_digest not tag
module SamsonHyperclair
class Engine < Rails::Engine
end

class << self
def append_job_with_scan(job, docker_tag)
return unless clair = ENV['HYPERCLAIR_PATH']

append_output job, "### Clair scan: started\n"

Thread.new do
ActiveRecord::Base.connection_pool.with_connection do
sleep 0.1 if Rails.env.test? # in test we reuse the same connection, so we cannot use it at the same time
success, output, time = scan(clair, job.project, docker_tag)
status = (success ? "success" : "errored or vulnerabilities found")
output = "### Clair scan: #{status} in #{time}s\n#{output}"
append_output job, output
end
end
end

private

def append_output(job, output)
job.reload
job.update_column(:output, job.output + output)
end

def scan(executable, project, docker_ref)
with_time do
Samson::CommandExecutor.execute(
executable,
*project.docker_repo(registry: :default).split('/', 2),
docker_ref,
whitelist_env: [
'DOCKER_REGISTRY_USER',
'DOCKER_REGISTRY_PASS',
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
'PATH'
],
timeout: 60 * 60
)
end
end

def with_time
result = []
time = Benchmark.realtime { result = yield }
result << time
end
end
end

Samson::Hooks.callback :after_docker_build do |build|
if build.docker_build_job.succeeded?
SamsonHyperclair.append_job_with_scan(build.docker_build_job, build.docker_ref)
end
end
6 changes: 6 additions & 0 deletions plugins/hyperclair/samson_hyperclair.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true
Gem::Specification.new "samson_hyperclair", "0.0.0" do |s|
s.summary = "Samson Hyperclair integration"
s.authors = ["Michael Grosser"]
s.email = "[email protected]"
end
79 changes: 79 additions & 0 deletions plugins/hyperclair/test/samson_hyperclair/samson_plugin_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true
require_relative '../test_helper'

SingleCov.covered! unless defined?(Rake) # rake preloads all plugins

describe SamsonHyperclair do
describe :after_docker_build do
let(:build) { builds(:docker_build) }

before { build.docker_build_job = jobs(:succeeded_test) }

it "runs clair" do
SamsonHyperclair.expects(:append_job_with_scan)
Samson::Hooks.fire(:after_docker_build, build)
end

it "does not run clair when build failed" do
build.docker_build_job.status = 'errored'
SamsonHyperclair.expects(:append_job_with_scan).never
Samson::Hooks.fire(:after_docker_build, build)
end
end

describe '.append_job_with_scan' do
share_database_connection_in_all_threads
with_registries ["docker-registry.example.com"]

def execute!
SamsonHyperclair.append_job_with_scan(job, 'latest')
end

let(:job) { jobs(:succeeded_test) }

around do |t|
Tempfile.open('clair') do |f|
f.write("#!/bin/bash\necho HELLO\nexit 0")
f.close
File.chmod 0o755, f.path
with_env(HYPERCLAIR_PATH: f.path, DOCKER_REGISTRY: 'my.registry', &t)
end
end

it "runs clair and reports success to the database" do
execute!

job.reload
job.output.must_include "Clair scan: started"

wait_for_threads

job.output.must_include "Clair scan: success"
job.output.must_include "HELLO"
end

it "runs clair and reports missing script to the database" do
File.unlink ENV['HYPERCLAIR_PATH']

execute!

wait_for_threads

job.reload
job.output.must_include "Clair scan: errored"
job.output.must_include "No such file or directory"
end

it "runs clair and reports failed script to the database" do
File.write ENV['HYPERCLAIR_PATH'], "#!/bin/bash\necho WORLD\nexit 1"

execute!

wait_for_threads

job.reload
job.output.must_include "Clair scan: errored"
job.output.must_include "WORLD"
end
end
end
2 changes: 2 additions & 0 deletions plugins/hyperclair/test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# frozen_string_literal: true
require_relative '../../../test/test_helper'
58 changes: 0 additions & 58 deletions test/lib/samson/clair_test.rb

This file was deleted.

Loading

0 comments on commit 459a5d5

Please sign in to comment.