mirror of
https://github.com/genuinetools/reg.git
synced 2024-05-11 17:18:32 -04:00
add clair integration tests
Signed-off-by: Jess Frazelle <acidburn@microsoft.com>
This commit is contained in:
parent
45db00a4fa
commit
73712bf067
11
Dockerfile.clair
Normal file
11
Dockerfile.clair
Normal 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
|
15
main_test.go
15
main_test.go
|
@ -62,11 +62,18 @@ func TestMain(m *testing.M) {
|
||||||
panic(fmt.Errorf("could not connect to docker: %v", err))
|
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 {
|
for _, regConfig := range registryConfigs {
|
||||||
// start each registry
|
// start each registry
|
||||||
regID, _, err := testutils.StartRegistry(dcli, regConfig.config, regConfig.username, regConfig.password)
|
regID, _, err := testutils.StartRegistry(dcli, regConfig.config, regConfig.username, regConfig.password)
|
||||||
if err != nil {
|
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))
|
panic(fmt.Errorf("starting registry container %s failed: %v", regConfig.config, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,11 +86,17 @@ func TestMain(m *testing.M) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if merr != 0 {
|
if merr != 0 {
|
||||||
|
testutils.RemoveContainer(dcli, dbID, clairID)
|
||||||
fmt.Printf("testing config %s failed\n", regConfig.config)
|
fmt.Printf("testing config %s failed\n", regConfig.config)
|
||||||
os.Exit(merr)
|
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)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -198,7 +198,8 @@ func (r *Registry) Headers(uri string) (map[string]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(token) < 1 {
|
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{
|
return map[string]string{
|
||||||
|
|
75
testutils/configs/clair.yml
Normal file
75
testutils/configs/clair.yml
Normal 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:
|
|
@ -1,6 +1,9 @@
|
||||||
package testutils
|
package testutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
@ -8,6 +11,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"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), "configs", "htpasswd") + ":" + "/etc/docker/registry/htpasswd" + ":ro",
|
||||||
filepath.Join(filepath.Dir(filename), "snakeoil") + ":" + "/etc/docker/registry/ssl" + ":ro",
|
filepath.Join(filepath.Dir(filename), "snakeoil") + ":" + "/etc/docker/registry/ssl" + ":ro",
|
||||||
},
|
},
|
||||||
|
RestartPolicy: container.RestartPolicy{
|
||||||
|
Name: "always",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
nil, "")
|
nil, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -73,7 +80,7 @@ func StartRegistry(dcli *client.Client, config, username, password string) (stri
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefill the images.
|
// 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 {
|
for _, image := range images {
|
||||||
if err := prefillRegistry(image, dcli, "localhost"+port, username, password); err != nil {
|
if err := prefillRegistry(image, dcli, "localhost"+port, username, password); err != nil {
|
||||||
return r.ID, addr, err
|
return r.ID, addr, err
|
||||||
|
@ -83,13 +90,115 @@ func StartRegistry(dcli *client.Client, config, username, password string) (stri
|
||||||
return r.ID, addr, nil
|
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.
|
// RemoveContainer removes with force a container by it's container ID.
|
||||||
func RemoveContainer(dcli *client.Client, ctrID string) error {
|
func RemoveContainer(dcli *client.Client, ctrs ...string) (err error) {
|
||||||
return dcli.ContainerRemove(context.Background(), ctrID,
|
for _, c := range ctrs {
|
||||||
types.ContainerRemoveOptions{
|
err = dcli.ContainerRemove(context.Background(), c,
|
||||||
RemoveVolumes: true,
|
types.ContainerRemoveOptions{
|
||||||
Force: true,
|
RemoveVolumes: true,
|
||||||
})
|
Force: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// dockerLogin logins via the command line to a docker registry
|
// 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
|
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
18
vulns_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue