add clair integration tests

Signed-off-by: Jess Frazelle <acidburn@microsoft.com>
This commit is contained in:
Jess Frazelle 2018-06-12 10:04:35 -04:00
parent 45db00a4fa
commit 73712bf067
No known key found for this signature in database
GPG key ID: 18F3685C0022BFF3
6 changed files with 282 additions and 9 deletions

11
Dockerfile.clair Normal file
View file

@ -0,0 +1,11 @@
FROM quay.io/coreos/clair
RUN apk add --no-cache \
ca-certificates
COPY testutils/snakeoil/cert.pem /usr/local/share/ca-certificates/clair.pem
# normally we'd use update-ca-certificates, but something about running it in
# Alpine is off, and the certs don't get added. Fortunately, we only need to
# add ca-certificates to the global store and it's all plain text.
RUN cat /usr/local/share/ca-certificates/* >> /etc/ssl/certs/ca-certificates.crt

View file

@ -62,11 +62,18 @@ func TestMain(m *testing.M) {
panic(fmt.Errorf("could not connect to docker: %v", err))
}
// start the clair containers.
dbID, clairID, err := testutils.StartClair(dcli)
if err != nil {
testutils.RemoveContainer(dcli, dbID, clairID)
panic(fmt.Errorf("starting clair containers failed: %v", err))
}
for _, regConfig := range registryConfigs {
// start each registry
regID, _, err := testutils.StartRegistry(dcli, regConfig.config, regConfig.username, regConfig.password)
if err != nil {
testutils.RemoveContainer(dcli, regID)
testutils.RemoveContainer(dcli, dbID, clairID, regID)
panic(fmt.Errorf("starting registry container %s failed: %v", regConfig.config, err))
}
@ -79,11 +86,17 @@ func TestMain(m *testing.M) {
}
if merr != 0 {
testutils.RemoveContainer(dcli, dbID, clairID)
fmt.Printf("testing config %s failed\n", regConfig.config)
os.Exit(merr)
}
}
// remove clair containers.
if err := testutils.RemoveContainer(dcli, dbID, clairID); err != nil {
log.Printf("couldn't remove clair containers: %v", err)
}
os.Exit(0)
}

View file

@ -198,7 +198,8 @@ func (r *Registry) Headers(uri string) (map[string]string, error) {
}
if len(token) < 1 {
return nil, fmt.Errorf("got empty token for %s", uri)
r.Logf("got empty token for %s", uri)
return map[string]string{}, nil
}
return map[string]string{

View file

@ -0,0 +1,75 @@
# Copyright 2015 clair authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# The values specified here are the default values that Clair uses if no configuration file is specified or if the keys are not defined.
clair:
database:
# Database driver
type: pgsql
options:
# PostgreSQL Connection string
# https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
source: postgresql://hacker:password@127.0.0.1:5432/clair?sslmode=disable&statement_timeout=60000
# Number of elements kept in the cache
# Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database.
cachesize: 16384
api:
# API server port
addr: "0.0.0.0:6060"
# Health server port
# This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
healthaddr: "0.0.0.0:6061"
# Deadline before an API request will respond with a 503
timeout: 900s
# Optional PKI configuration
# If you want to easily generate client certificates and CAs, try the following projects:
# https://github.com/coreos/etcd-ca
# https://github.com/cloudflare/cfssl
servername:
cafile:
keyfile:
certfile:
updater:
# Frequency the database will be updated with vulnerabilities from the default data sources
# The value 0 disables the updater entirely.
interval: 2h
notifier:
# Number of attempts before the notification is marked as failed to be sent
attempts: 3
# Duration before a failed notification is retried
renotifyinterval: 2h
http:
# Optional endpoint that will receive notifications via POST requests
endpoint:
# Optional PKI configuration
# If you want to easily generate client certificates and CAs, try the following projects:
# https://github.com/cloudflare/cfssl
# https://github.com/coreos/etcd-ca
servername:
cafile:
keyfile:
certfile:
# Optional HTTP Proxy: must be a valid URL (including the scheme).
proxy:

View file

@ -1,6 +1,9 @@
package testutils
import (
"archive/tar"
"bufio"
"bytes"
"context"
"crypto/tls"
"crypto/x509"
@ -8,6 +11,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
@ -50,6 +54,9 @@ func StartRegistry(dcli *client.Client, config, username, password string) (stri
filepath.Join(filepath.Dir(filename), "configs", "htpasswd") + ":" + "/etc/docker/registry/htpasswd" + ":ro",
filepath.Join(filepath.Dir(filename), "snakeoil") + ":" + "/etc/docker/registry/ssl" + ":ro",
},
RestartPolicy: container.RestartPolicy{
Name: "always",
},
},
nil, "")
if err != nil {
@ -73,7 +80,7 @@ func StartRegistry(dcli *client.Client, config, username, password string) (stri
}
// Prefill the images.
images := []string{"alpine:latest", "busybox:latest", "busybox:musl", "busybox:glibc"}
images := []string{"alpine:3.5", "alpine:latest", "busybox:latest", "busybox:musl", "busybox:glibc"}
for _, image := range images {
if err := prefillRegistry(image, dcli, "localhost"+port, username, password); err != nil {
return r.ID, addr, err
@ -83,13 +90,115 @@ func StartRegistry(dcli *client.Client, config, username, password string) (stri
return r.ID, addr, nil
}
func startClairDB(dcli *client.Client) (string, error) {
image := "postgres:latest"
if err := pullDockerImage(dcli, image); err != nil {
return "", err
}
c, err := dcli.ContainerCreate(
context.Background(),
&container.Config{
Image: image,
Env: []string{
"POSTGRES_PASSWORD=password",
"POSTGRES_DB=clair",
"POSTGRES_USER=hacker",
},
},
&container.HostConfig{
NetworkMode: "host",
RestartPolicy: container.RestartPolicy{
Name: "always",
},
},
nil, "")
if err != nil {
return "", err
}
// start the container
return c.ID, dcli.ContainerStart(context.Background(), c.ID, types.ContainerStartOptions{})
}
// StartClair starts a new clair container and accompanying database.
func StartClair(dcli *client.Client) (string, string, error) {
_, filename, _, ok := runtime.Caller(0)
if !ok {
return "", "", errors.New("No caller information")
}
// start the database container.
dbID, err := startClairDB(dcli)
if err != nil {
return dbID, "", err
}
image := "clair:dev"
// build the docker image
// create the tar ball
ctx := filepath.Dir(filepath.Dir(filename))
tw, err := tarit(ctx)
if err != nil {
return dbID, "", err
}
// build the image
resp, err := dcli.ImageBuild(context.Background(), tw, types.ImageBuildOptions{
Tags: []string{image},
Dockerfile: "Dockerfile.clair",
ForceRemove: true,
Remove: true,
SuppressOutput: false,
PullParent: true,
})
if err != nil {
return dbID, "", err
}
defer resp.Body.Close()
c, err := dcli.ContainerCreate(
context.Background(),
&container.Config{
Image: image,
},
&container.HostConfig{
NetworkMode: "host",
Binds: []string{
filepath.Join(filepath.Dir(filename), "configs", "clair.yml") + ":" + "/etc/clair/config.yaml" + ":ro",
},
RestartPolicy: container.RestartPolicy{
Name: "always",
},
},
nil, "")
if err != nil {
return dbID, c.ID, err
}
// start the container
err = dcli.ContainerStart(context.Background(), c.ID, types.ContainerStartOptions{})
// wait for clair to start
// TODO: make this not a sleep
time.Sleep(time.Second * 5)
return dbID, c.ID, err
}
// RemoveContainer removes with force a container by it's container ID.
func RemoveContainer(dcli *client.Client, ctrID string) error {
return dcli.ContainerRemove(context.Background(), ctrID,
types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
})
func RemoveContainer(dcli *client.Client, ctrs ...string) (err error) {
for _, c := range ctrs {
err = dcli.ContainerRemove(context.Background(), c,
types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
})
}
return err
}
// dockerLogin logins via the command line to a docker registry
@ -239,3 +348,49 @@ func constructRegistryAuth(identity, secret string) (string, error) {
return base64.URLEncoding.EncodeToString(buf), nil
}
func tarit(src string) (io.Reader, error) {
s := bytes.NewBuffer(nil)
t := bytes.NewBuffer(nil)
buf := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(t))
tarball := tar.NewWriter(s)
err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return err
}
header.Name = strings.TrimPrefix(path, src)
if err := tarball.WriteHeader(header); err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(tarball, file)
return err
})
if err != nil {
return nil, err
}
if _, err := s.WriteTo(buf); err != nil {
return nil, err
}
err = buf.Writer.Flush()
return t, err
}

18
vulns_test.go Normal file
View file

@ -0,0 +1,18 @@
package main
import (
"strings"
"testing"
)
func TestVulns(t *testing.T) {
out, err := run("vulns", "--clair", "http://localhost:6060", "alpine:3.5")
if err != nil {
t.Fatalf("output: %s, error: %v", string(out), err)
}
expected := `clair.clair resp.Status=200 OK`
if !strings.HasSuffix(strings.TrimSpace(out), expected) {
t.Logf("expected: %s\ngot: %s", expected, out)
}
}