migrate hyperclair into cmd/clairclt package

This commit is contained in:
jgsqware 2016-04-27 18:27:55 +02:00
parent 458d59df46
commit 1f3601c6da
53 changed files with 22417 additions and 0 deletions

21
cmd/clairclt/LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 wemanity-belgium
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
cmd/clairclt/VERSION Normal file
View File

@ -0,0 +1 @@
0.4.0

View File

@ -0,0 +1,39 @@
package clair
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/coreos/clair/api/v1"
)
//Analyse get Analysis os specified layer
func Analyse(id string) (v1.LayerEnvelope, error) {
lURI := fmt.Sprintf("%v/layers/%v?vulnerabilities", uri, id)
// lURI := fmt.Sprintf("%v/layers/%v/vulnerabilities?minimumPriority=%v", uri, id, priority)
response, err := http.Get(lURI)
if err != nil {
return v1.LayerEnvelope{}, fmt.Errorf("analysing layer %v: %v", id, err)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return v1.LayerEnvelope{}, fmt.Errorf("reading layer analysis: %v", err)
}
if response.StatusCode != 200 {
return v1.LayerEnvelope{}, fmt.Errorf("%d - %s", response.StatusCode, string(body))
}
var analysis v1.LayerEnvelope
err = json.Unmarshal(body, &analysis)
if err != nil {
return v1.LayerEnvelope{}, fmt.Errorf("unmarshalling layer analysis: %v", err)
}
return analysis, nil
}

103
cmd/clairclt/clair/clair.go Normal file
View File

@ -0,0 +1,103 @@
package clair
import (
"strconv"
"strings"
"github.com/coreos/clair/api/v1"
"github.com/coreos/clair/cmd/clairclt/xstrings"
"github.com/spf13/viper"
)
var uri string
var priority string
var healthPort int
//Report Reporting Config value
var Report ReportConfig
//ImageAnalysis Full image analysis
type ImageAnalysis struct {
Registry string
ImageName string
Tag string
Layers []v1.LayerEnvelope
}
func (imageAnalysis ImageAnalysis) String() string {
return imageAnalysis.Registry + "/" + imageAnalysis.ImageName + ":" + imageAnalysis.Tag
}
func (imageAnalysis ImageAnalysis) ShortName(l v1.Layer) string {
return xstrings.Substr(l.Name, 0, 12)
}
func (imageAnalysis ImageAnalysis) CountVulnerabilities(l v1.Layer) int {
count := 0
for _, f := range l.Features {
count += len(f.Vulnerabilities)
}
return count
}
type Vulnerability struct {
Name, Severity, IntroduceBy, Description, Layer string
}
func (imageAnalysis ImageAnalysis) SortVulnerabilities() []Vulnerability {
low := []Vulnerability{}
medium := []Vulnerability{}
high := []Vulnerability{}
critical := []Vulnerability{}
defcon1 := []Vulnerability{}
for _, l := range imageAnalysis.Layers {
for _, f := range l.Layer.Features {
for _, v := range f.Vulnerabilities {
nv := Vulnerability{
Name: v.Name,
Severity: v.Severity,
IntroduceBy: f.Name + ":" + f.Version,
Description: v.Description,
Layer: l.Layer.Name,
}
switch strings.ToLower(v.Severity) {
case "low":
low = append(low, nv)
case "medium":
medium = append(medium, nv)
case "high":
high = append(high, nv)
case "critical":
critical = append(critical, nv)
case "defcon1":
defcon1 = append(defcon1, nv)
}
}
}
}
return append(defcon1, append(critical, append(high, append(medium, low...)...)...)...)
}
func fmtURI(u string, port int) {
uri = u
if port != 0 {
uri += ":" + strconv.Itoa(port)
}
if !strings.HasSuffix(uri, "/v1") {
uri += "/v1"
}
if !strings.HasPrefix(uri, "http://") && !strings.HasPrefix(uri, "https://") {
uri = "http://" + uri
}
}
//Config configure Clair from configFile
func Config() {
fmtURI(viper.GetString("clair.uri"), viper.GetInt("clair.port"))
priority = viper.GetString("clair.priority")
healthPort = viper.GetInt("clair.healthPort")
Report.Path = viper.GetString("clair.report.path")
Report.Format = viper.GetString("clair.report.format")
}

View File

@ -0,0 +1,31 @@
package clair
import (
"net/http"
"net/http/httptest"
"testing"
)
func newServer(httpStatus int) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(httpStatus)
}))
}
func TestIsHealthy(t *testing.T) {
server := newServer(http.StatusOK)
defer server.Close()
uri = server.URL
if h := IsHealthy(); !h {
t.Errorf("IsHealthy() => %v, want %v", h, true)
}
}
func TestIsNotHealthy(t *testing.T) {
server := newServer(http.StatusInternalServerError)
defer server.Close()
uri = server.URL
if h := IsHealthy(); h {
t.Errorf("IsHealthy() => %v, want %v", h, true)
}
}

View File

@ -0,0 +1,26 @@
package clair
import (
"fmt"
"net/http"
"os"
"strconv"
"strings"
)
func IsHealthy() bool {
healthURI := strings.Replace(uri, "6060/v1", strconv.Itoa(healthPort), 1) + "/health"
response, err := http.Get(healthURI)
if err != nil {
fmt.Fprintf(os.Stderr, "requesting Clair health: %v", err)
return false
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return false
}
return true
}

View File

@ -0,0 +1,59 @@
package clair
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"github.com/coreos/clair/api/v1"
)
//ErrOSNotSupported is returned when Clair received a layer which on os not supported
var ErrOSNotSupported = errors.New("worker: OS and/or package manager are not supported")
//Push send a layer to Clair for analysis
func Push(layer v1.LayerEnvelope) error {
lJSON, err := json.Marshal(layer)
if err != nil {
return fmt.Errorf("marshalling layer: %v", err)
}
lURI := fmt.Sprintf("%v/layers", uri)
request, err := http.NewRequest("POST", lURI, bytes.NewBuffer(lJSON))
if err != nil {
return fmt.Errorf("creating 'add layer' request: %v", err)
}
request.Header.Set("Content-Type", "application/json")
response, err := (&http.Client{}).Do(request)
if err != nil {
return fmt.Errorf("pushing layer to clair: %v", err)
}
defer response.Body.Close()
if response.StatusCode != 201 {
if response.StatusCode == 422 {
return ErrOSNotSupported
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("reading 'add layer' response : %v", err)
}
type layerError struct {
Message string
}
var lErr layerError
err = json.Unmarshal(body, &lErr)
if err != nil {
return fmt.Errorf("unmarshalling 'add layer' error message: %v", err)
}
return fmt.Errorf("%d - %s", response.StatusCode, string(body))
}
return nil
}

View File

@ -0,0 +1,32 @@
package clair
import (
"bytes"
"fmt"
"text/template"
)
//go:generate go-bindata -pkg clair -o templates.go templates/...
//ReportConfig Reporting configuration
type ReportConfig struct {
Path string
Format string
}
//ReportAsHTML report analysis as HTML
func ReportAsHTML(analyses ImageAnalysis) (string, error) {
asset, err := Asset("templates/analysis-template.html")
if err != nil {
return "", fmt.Errorf("accessing template: %v", err)
}
templte := template.Must(template.New("analysis-template").Parse(string(asset)))
var doc bytes.Buffer
err = templte.Execute(&doc, analyses)
if err != nil {
return "", fmt.Errorf("rendering HTML report: %v", err)
}
return doc.String(), nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,32 @@
package clair
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func Versions() (interface{}, error) {
Config()
response, err := http.Get(uri + "/versions")
if err != nil {
return nil, fmt.Errorf("requesting Clair version: %v", err)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("reading Clair version body: %v", err)
}
var versionBody interface{}
err = json.Unmarshal(body, &versionBody)
if err != nil {
return nil, fmt.Errorf("unmarshalling Clair version body: %v", err)
}
return versionBody, nil
}

BIN
cmd/clairclt/clairclt Executable file

Binary file not shown.

View File

@ -0,0 +1,81 @@
package cmd
import (
"fmt"
"os"
"text/template"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/cmd/clairclt/clair"
"github.com/coreos/clair/cmd/clairclt/docker"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const analyseTplt = `
Image: {{.String}}
{{.Layers | len}} layers found
{{$ia := .}}
{{range .Layers}} {{with .Layer}}Analysis [{{.|$ia.ShortName}}] found {{.|$ia.CountVulnerabilities}} vulnerabilities.{{end}}
{{end}}
`
var analyseCmd = &cobra.Command{
Use: "analyse IMAGE",
Short: "Analyse Docker image",
Long: `Analyse a Docker image with Clair, against Ubuntu, Red hat and Debian vulnerabilities databases`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Printf("hyperclair: \"analyse\" requires a minimum of 1 argument")
os.Exit(1)
}
ia := analyse(args[0])
err := template.Must(template.New("analysis").Parse(analyseTplt)).Execute(os.Stdout, ia)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("rendering analysis: %v", err)
}
},
}
func analyse(imageName string) clair.ImageAnalysis {
var err error
var image docker.Image
if !docker.IsLocal {
image, err = docker.Pull(imageName)
if err != nil {
if err == docker.ErrNotFound {
fmt.Println(err)
} else {
fmt.Println(errInternalError)
}
logrus.Fatalf("pulling image %q: %v", imageName, err)
}
} else {
image, err = docker.Parse(imageName)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("parsing local image %q: %v", imageName, err)
}
docker.FromHistory(&image)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("getting local image %q from history: %v", imageName, err)
}
}
return docker.Analyse(image)
}
func init() {
RootCmd.AddCommand(analyseCmd)
analyseCmd.Flags().BoolVarP(&docker.IsLocal, "local", "l", false, "Use local images")
analyseCmd.Flags().StringP("priority", "p", "Low", "Vulnerabilities priority [Low, Medium, High, Critical]")
viper.BindPFlag("clair.priority", analyseCmd.Flags().Lookup("priority"))
}

View File

@ -0,0 +1,38 @@
package cmd
import (
"fmt"
"os"
"text/template"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/cmd/clairclt/clair"
"github.com/spf13/cobra"
)
const healthTplt = `
Clair: {{if .}}{{else}}{{end}}
`
type health struct {
Clair interface{} `json:"clair"`
}
var healthCmd = &cobra.Command{
Use: "health",
Short: "Get Health of Hyperclair and underlying services",
Long: `Get Health of Hyperclair and underlying services`,
Run: func(cmd *cobra.Command, args []string) {
ok := clair.IsHealthy()
err := template.Must(template.New("health").Parse(healthTplt)).Execute(os.Stdout, ok)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("rendering the health: %v", err)
}
},
}
func init() {
RootCmd.AddCommand(healthCmd)
}

76
cmd/clairclt/cmd/login.go Normal file
View File

@ -0,0 +1,76 @@
package cmd
import (
"encoding/base64"
"fmt"
"os"
"golang.org/x/crypto/ssh/terminal"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/cmd/clairclt/config"
"github.com/coreos/clair/cmd/clairclt/docker"
"github.com/spf13/cobra"
)
var loginCmd = &cobra.Command{
Use: "login",
Short: "Log in to a Docker registry",
Long: `Log in to a Docker registry`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 1 {
fmt.Println("Only one argument is allowed")
os.Exit(1)
}
var reg string = docker.DockerHub
if len(args) == 1 {
reg = args[0]
}
var login config.Login
if err := askForLogin(&login); err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("encrypting password: %v", err)
}
config.AddLogin(reg, login)
logged, err := docker.Login(reg)
if err != nil && err != docker.ErrUnauthorized {
config.RemoveLogin(reg)
fmt.Println(errInternalError)
logrus.Fatalf("log in: %v", err)
}
if !logged {
config.RemoveLogin(reg)
fmt.Println("Unauthorized: Wrong login/password, please try again")
os.Exit(1)
}
fmt.Println("Login Successful")
},
}
func askForLogin(login *config.Login) error {
fmt.Print("Username: ")
fmt.Scan(&login.Username)
fmt.Print("Password: ")
pwd, err := terminal.ReadPassword(1)
if err != nil {
return err
}
fmt.Println(" ")
encryptedPwd := base64.StdEncoding.EncodeToString(pwd)
login.Password = string(encryptedPwd)
return nil
}
func init() {
RootCmd.AddCommand(loginCmd)
}

View File

@ -0,0 +1,44 @@
package cmd
import (
"fmt"
"os"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/cmd/clairclt/config"
"github.com/coreos/clair/cmd/clairclt/docker"
"github.com/spf13/cobra"
)
var logoutCmd = &cobra.Command{
Use: "logout",
Short: "Log out from a Docker registry",
Long: `Log out from a Docker registry`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 1 {
fmt.Println("Only one argument is allowed")
os.Exit(1)
}
var reg string = docker.DockerHub
if len(args) == 1 {
reg = args[0]
}
ok, err := config.RemoveLogin(reg)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("log out: %v", err)
}
if ok {
fmt.Println("Log out successful")
return
}
fmt.Println("You are not logged in")
},
}
func init() {
RootCmd.AddCommand(logoutCmd)
}

62
cmd/clairclt/cmd/pull.go Normal file
View File

@ -0,0 +1,62 @@
// Copyright © 2016 NAME HERE <EMAIL ADDRESS>
//
// 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.
package cmd
import (
"fmt"
"os"
"text/template"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/cmd/clairclt/docker"
"github.com/spf13/cobra"
)
const pullTplt = `
Image: {{.String}}
{{.FsLayers | len}} layers found
{{range .FsLayers}} {{.BlobSum}}
{{end}}
`
// pingCmd represents the ping command
var pullCmd = &cobra.Command{
Use: "pull IMAGE",
Short: "Pull Docker image information",
Long: `Pull image information from Docker Hub or Registry`,
Run: func(cmd *cobra.Command, args []string) {
//TODO how to use args with viper
if len(args) != 1 {
fmt.Printf("hyperclair: \"pull\" requires a minimum of 1 argument\n")
os.Exit(1)
}
im := args[0]
image, err := docker.Pull(im)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("pulling image %v: %v", args[0], err)
}
err = template.Must(template.New("pull").Parse(pullTplt)).Execute(os.Stdout, image)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("rendering image: %v", err)
}
},
}
func init() {
RootCmd.AddCommand(pullCmd)
}

86
cmd/clairclt/cmd/push.go Normal file
View File

@ -0,0 +1,86 @@
package cmd
import (
"fmt"
"os"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/cmd/clairclt/config"
"github.com/coreos/clair/cmd/clairclt/docker"
"github.com/coreos/clair/cmd/clairclt/server"
"github.com/spf13/cobra"
)
var pushCmd = &cobra.Command{
Use: "push IMAGE",
Short: "Push Docker image to Clair",
Long: `Upload a Docker image to Clair for further analysis`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Printf("hyperclair: \"push\" requires a minimum of 1 argument\n")
os.Exit(1)
}
startLocalServer()
imageName := args[0]
var image docker.Image
if !docker.IsLocal {
var err error
image, err = docker.Pull(imageName)
if err != nil {
if err == docker.ErrNotFound {
fmt.Println(err)
} else {
fmt.Println(errInternalError)
}
logrus.Fatalf("pulling image %q: %v", imageName, err)
}
} else {
var err error
image, err = docker.Parse(imageName)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("parsing local image %q: %v", imageName, err)
}
err = docker.Prepare(&image)
logrus.Debugf("prepared image layers: %d", len(image.FsLayers))
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("preparing local image %q from history: %v", imageName, err)
}
}
logrus.Info("Pushing Image")
if err := docker.Push(image); err != nil {
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("pushing image %q: %v", imageName, err)
}
}
fmt.Printf("%v has been pushed to Clair\n", imageName)
},
}
func init() {
RootCmd.AddCommand(pushCmd)
pushCmd.Flags().BoolVarP(&docker.IsLocal, "local", "l", false, "Use local images")
}
//StartLocalServer start the hyperclair local server needed for reverse proxy and file server
func startLocalServer() {
sURL, err := config.LocalServerIP()
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("retrieving internal server IP: %v", err)
}
err = server.Serve(sURL)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("starting local server: %v", err)
}
}

View File

@ -0,0 +1,87 @@
package cmd
import (
"fmt"
"os"
"strings"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/cmd/clairclt/clair"
"github.com/coreos/clair/cmd/clairclt/docker"
"github.com/coreos/clair/cmd/clairclt/xstrings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var reportCmd = &cobra.Command{
Use: "report IMAGE",
Short: "Generate Docker Image vulnerabilities report",
Long: `Generate Docker Image vulnerabilities report as HTML or JSON`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Printf("hyperclair: \"report\" requires a minimum of 1 argument")
os.Exit(1)
}
analyses := analyse(args[0])
imageName := strings.Replace(analyses.ImageName, "/", "-", -1) + "-" + analyses.Tag
switch clair.Report.Format {
case "html":
html, err := clair.ReportAsHTML(analyses)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("generating HTML report: %v", err)
}
err = saveReport(imageName, string(html))
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("saving HTML report: %v", err)
}
case "json":
json, err := xstrings.ToIndentJSON(analyses)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("indenting JSON: %v", err)
}
err = saveReport(imageName, string(json))
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("saving JSON report: %v", err)
}
default:
fmt.Printf("Unsupported Report format: %v", clair.Report.Format)
logrus.Fatalf("Unsupported Report format: %v", clair.Report.Format)
}
},
}
func saveReport(name string, content string) error {
path := viper.GetString("clair.report.path") + "/" + clair.Report.Format
if err := os.MkdirAll(path, 0777); err != nil {
return err
}
reportsName := fmt.Sprintf("%v/analysis-%v.%v", path, name, strings.ToLower(clair.Report.Format))
f, err := os.Create(reportsName)
if err != nil {
return fmt.Errorf("creating report file: %v", err)
}
_, err = f.WriteString(content)
if err != nil {
return fmt.Errorf("writing report file: %v", err)
}
fmt.Printf("%v report at %v\n", strings.ToUpper(clair.Report.Format), reportsName)
return nil
}
func init() {
RootCmd.AddCommand(reportCmd)
reportCmd.Flags().BoolVarP(&docker.IsLocal, "local", "l", false, "Use local images")
reportCmd.Flags().StringP("format", "f", "html", "Format for Report [html,json]")
viper.BindPFlag("clair.report.format", reportCmd.Flags().Lookup("format"))
}

61
cmd/clairclt/cmd/root.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright © 2016 NAME HERE <EMAIL ADDRESS>
//
// 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.
package cmd
import (
"errors"
"fmt"
"os"
"github.com/coreos/clair/cmd/clairclt/config"
"github.com/spf13/cobra"
)
var (
errInternalError = errors.New("client quit unexpectedly")
)
var cfgFile string
var logLevel string
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "hyperclair",
Short: "Analyse your docker image with Clair, directly from your registry.",
Long: ``,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) {
// },
}
// Execute adds all child commands to the root command sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(-1)
}
}
func init() {
cobra.OnInitialize(initConfig)
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.hyperclair.yml)")
RootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "", "log level [Panic,Fatal,Error,Warn,Info,Debug]")
}
func initConfig() {
config.Init(cfgFile, logLevel)
}

View File

@ -0,0 +1,36 @@
package cmd
import (
"fmt"
"os"
"text/template"
"github.com/Sirupsen/logrus"
"github.com/spf13/cobra"
)
const versionTplt = `
Hyperclair version {{.}}
`
var version string
var templ = template.Must(template.New("versions").Parse(versionTplt))
var versionCmd = &cobra.Command{
Use: "version",
Short: "Get Versions of Hyperclair and underlying services",
Long: `Get Versions of Hyperclair and underlying services`,
Run: func(cmd *cobra.Command, args []string) {
err := templ.Execute(os.Stdout, version)
if err != nil {
fmt.Println(errInternalError)
logrus.Fatalf("rendering the version: %v", err)
}
},
}
func init() {
RootCmd.AddCommand(versionCmd)
}

View File

@ -0,0 +1,312 @@
package config
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"os/user"
"strings"
"gopkg.in/yaml.v2"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/cmd/clairclt/clair"
"github.com/coreos/clair/cmd/clairclt/xstrings"
"github.com/spf13/viper"
)
var ErrLoginNotFound = errors.New("user is not log in")
type r struct {
Path, Format string
}
type c struct {
URI, Priority string
Port, HealthPort int
Report r
}
type a struct {
InsecureSkipVerify bool
}
type h struct {
IP, TempFolder string
Port int
}
type config struct {
Clair c
Auth a
Hyperclair h
}
// Init reads in config file and ENV variables if set.
func Init(cfgFile string, logLevel string) {
lvl := logrus.WarnLevel
if logLevel != "" {
var err error
lvl, err = logrus.ParseLevel(logLevel)
if err != nil {
logrus.Warningf("Wrong Log level %v, defaults to [Warning]", logLevel)
lvl = logrus.WarnLevel
}
}
logrus.SetLevel(lvl)
viper.SetEnvPrefix("hyperclair")
viper.SetConfigName("hyperclair") // name of config file (without extension)
viper.AddConfigPath("$HOME/.hyperclair") // adding home directory as first search path
viper.AddConfigPath(".") // adding home directory as first search path
viper.AutomaticEnv() // read in environment variables that match
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
}
err := viper.ReadInConfig()
if err != nil {
logrus.Debugf("No config file used")
} else {
logrus.Debugf("Using config file: %v", viper.ConfigFileUsed())
}
if viper.Get("clair.uri") == nil {
viper.Set("clair.uri", "http://localhost")
}
if viper.Get("clair.port") == nil {
viper.Set("clair.port", "6060")
}
if viper.Get("clair.healthPort") == nil {
viper.Set("clair.healthPort", "6061")
}
if viper.Get("clair.priority") == nil {
viper.Set("clair.priority", "Low")
}
if viper.Get("clair.report.path") == nil {
viper.Set("clair.report.path", "reports")
}
if viper.Get("clair.report.format") == nil {
viper.Set("clair.report.format", "html")
}
if viper.Get("auth.insecureSkipVerify") == nil {
viper.Set("auth.insecureSkipVerify", "true")
}
if viper.Get("hyperclair.ip") == nil {
viper.Set("hyperclair.ip", "")
}
if viper.Get("hyperclair.port") == nil {
viper.Set("hyperclair.port", 0)
}
if viper.Get("hyperclair.tempFolder") == nil {
viper.Set("hyperclair.tempFolder", "/tmp/hyperclair")
}
clair.Config()
}
func values() config {
return config{
Clair: c{
URI: viper.GetString("clair.uri"),
Port: viper.GetInt("clair.port"),
HealthPort: viper.GetInt("clair.healthPort"),
Priority: viper.GetString("clair.priority"),
Report: r{
Path: viper.GetString("clair.report.path"),
Format: viper.GetString("clair.report.format"),
},
},
Auth: a{
InsecureSkipVerify: viper.GetBool("auth.insecureSkipVerify"),
},
Hyperclair: h{
IP: viper.GetString("hyperclair.ip"),
Port: viper.GetInt("hyperclair.port"),
TempFolder: viper.GetString("hyperclair.tempFolder"),
},
}
}
func Print() {
cfg := values()
cfgBytes, err := yaml.Marshal(cfg)
if err != nil {
logrus.Fatalf("marshalling configuration: %v", err)
}
fmt.Println("Configuration")
fmt.Printf("%v", string(cfgBytes))
}
func HyperclairHome() string {
usr, err := user.Current()
if err != nil {
panic(err)
}
p := usr.HomeDir + "/.hyperclair"
if _, err := os.Stat(p); os.IsNotExist(err) {
os.Mkdir(p, 0700)
}
return p
}
type Login struct {
Username string
Password string
}
type loginMapping map[string]Login
func HyperclairConfig() string {
return HyperclairHome() + "/config.json"
}
func AddLogin(registry string, login Login) error {
var logins loginMapping
if err := readConfigFile(&logins, HyperclairConfig()); err != nil {
return fmt.Errorf("reading hyperclair file: %v", err)
}
logins[registry] = login
if err := writeConfigFile(logins, HyperclairConfig()); err != nil {
return fmt.Errorf("indenting login: %v", err)
}
return nil
}
func GetLogin(registry string) (Login, error) {
if _, err := os.Stat(HyperclairConfig()); err == nil {
var logins loginMapping
if err := readConfigFile(&logins, HyperclairConfig()); err != nil {
return Login{}, fmt.Errorf("reading hyperclair file: %v", err)
}
if login, present := logins[registry]; present {
d, err := base64.StdEncoding.DecodeString(login.Password)
if err != nil {
return Login{}, fmt.Errorf("decoding password: %v", err)
}
login.Password = string(d)
return login, nil
}
}
return Login{}, ErrLoginNotFound
}
func RemoveLogin(registry string) (bool, error) {
if _, err := os.Stat(HyperclairConfig()); err == nil {
var logins loginMapping
if err := readConfigFile(&logins, HyperclairConfig()); err != nil {
return false, fmt.Errorf("reading hyperclair file: %v", err)
}
if _, present := logins[registry]; present {
delete(logins, registry)
if err := writeConfigFile(logins, HyperclairConfig()); err != nil {
return false, fmt.Errorf("indenting login: %v", err)
}
return true, nil
}
}
return false, nil
}
func readConfigFile(logins *loginMapping, file string) error {
if _, err := os.Stat(file); err == nil {
f, err := ioutil.ReadFile(file)
if err != nil {
return err
}
if err := json.Unmarshal(f, &logins); err != nil {
return err
}
} else {
*logins = loginMapping{}
}
return nil
}
func writeConfigFile(logins loginMapping, file string) error {
s, err := xstrings.ToIndentJSON(logins)
if err != nil {
return err
}
err = ioutil.WriteFile(file, s, os.ModePerm)
if err != nil {
return err
}
return nil
}
//LocalServerIP return the local hyperclair server IP
func LocalServerIP() (string, error) {
localPort := viper.GetString("hyperclair.port")
localIP := viper.GetString("hyperclair.ip")
if localIP == "" {
logrus.Infoln("retrieving docker0 interface as local IP")
var err error
localIP, err = Docker0InterfaceIP()
if err != nil {
return "", fmt.Errorf("retrieving docker0 interface ip: %v", err)
}
}
return strings.TrimSpace(localIP) + ":" + localPort, nil
}
//Docker0InterfaceIP return the docker0 interface ip by running `ip route show | grep docker0 | awk {print $9}`
func Docker0InterfaceIP() (string, error) {
var localIP bytes.Buffer
ip := exec.Command("ip", "route", "show")
rGrep, wIP := io.Pipe()
grep := exec.Command("grep", "docker0")
ip.Stdout = wIP
grep.Stdin = rGrep
awk := exec.Command("awk", "{print $9}")
rAwk, wGrep := io.Pipe()
grep.Stdout = wGrep
awk.Stdin = rAwk
awk.Stdout = &localIP
err := ip.Start()
if err != nil {
return "", err
}
err = grep.Start()
if err != nil {
return "", err
}
err = awk.Start()
if err != nil {
return "", err
}
err = ip.Wait()
if err != nil {
return "", err
}
err = wIP.Close()
if err != nil {
return "", err
}
err = grep.Wait()
if err != nil {
return "", err
}
err = wGrep.Close()
if err != nil {
return "", err
}
err = awk.Wait()
if err != nil {
return "", err
}
return localIP.String(), nil
}

View File

@ -0,0 +1,204 @@
package config
import (
"fmt"
"io/ioutil"
"log"
"os"
"testing"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
)
var loginData = []struct {
in string
out int
}{
{"", 0},
{`{
"docker.io": {
"Username": "johndoe",
"Password": "$2a$05$Qe4TTO8HMmOht"
}
}
`, 1},
}
const defaultValues = `
clair:
uri: http://localhost
priority: Low
port: 6060
healthport: 6061
report:
path: reports
format: html
auth:
insecureskipverify: true
hyperclair:
ip: ""
tempfolder: /tmp/hyperclair
port: 0
`
const customValues = `
clair:
uri: http://clair
priority: High
port: 6061
healthport: 6062
report:
path: reports/test
format: json
auth:
insecureskipverify: false
hyperclair:
ip: "localhost"
tempfolder: /tmp/hyperclair/test
port: 64157
`
func TestInitDefault(t *testing.T) {
Init("", "INFO")
cfg := values()
var expected config
err := yaml.Unmarshal([]byte(defaultValues), &expected)
if err != nil {
t.Fatal(err)
}
if cfg != expected {
t.Error("Default values are not correct")
}
viper.Reset()
}
func TestInitCustomLocal(t *testing.T) {
tmpfile := CreateConfigFile(customValues, "hyperclair.yml", ".")
defer os.Remove(tmpfile) // clean up
fmt.Println(tmpfile)
Init("", "INFO")
cfg := values()
var expected config
err := yaml.Unmarshal([]byte(customValues), &expected)
if err != nil {
t.Fatal(err)
}
if cfg != expected {
t.Error("values are not correct")
}
viper.Reset()
}
func TestInitCustomHome(t *testing.T) {
tmpfile := CreateConfigFile(customValues, "hyperclair.yml", HyperclairHome())
defer os.Remove(tmpfile) // clean up
fmt.Println(tmpfile)
Init("", "INFO")
cfg := values()
var expected config
err := yaml.Unmarshal([]byte(customValues), &expected)
if err != nil {
t.Fatal(err)
}
if cfg != expected {
t.Error("values are not correct")
}
viper.Reset()
}
func TestInitCustom(t *testing.T) {
tmpfile := CreateConfigFile(customValues, "hyperclair.yml", "/tmp")
defer os.Remove(tmpfile) // clean up
fmt.Println(tmpfile)
Init(tmpfile, "INFO")
cfg := values()
var expected config
err := yaml.Unmarshal([]byte(customValues), &expected)
if err != nil {
t.Fatal(err)
}
if cfg != expected {
t.Error("values are not correct")
}
viper.Reset()
}
func TestReadConfigFile(t *testing.T) {
for _, ld := range loginData {
tmpfile := CreateTmpConfigFile(ld.in)
defer os.Remove(tmpfile) // clean up
var logins loginMapping
if err := readConfigFile(&logins, tmpfile); err != nil {
t.Errorf("readConfigFile(&logins,%q) failed => %v", tmpfile, err)
}
if l := len(logins); l != ld.out {
t.Errorf("readConfigFile(&logins,%q) => %v logins, want %v", tmpfile, l, ld.out)
}
}
}
func TestWriteConfigFile(t *testing.T) {
logins := loginMapping{}
logins["docker.io"] = Login{Username: "johndoe", Password: "$2a$05$Qe4TTO8HMmOht"}
tmpfile := CreateTmpConfigFile("")
defer os.Remove(tmpfile) // clean up
if err := writeConfigFile(logins, tmpfile); err != nil {
t.Errorf("writeConfigFile(logins,%q) failed => %v", tmpfile, err)
}
logins = loginMapping{}
if err := readConfigFile(&logins, tmpfile); err != nil {
t.Errorf("after writing: readConfigFile(&logins,%q) failed => %v", tmpfile, err)
}
if l := len(logins); l != 1 {
t.Errorf("after writing: readConfigFile(&logins,%q) => %v logins, want %v", tmpfile, l, 1)
}
}
func CreateTmpConfigFile(content string) string {
c := []byte(content)
tmpfile, err := ioutil.TempFile("", "test-hyperclair")
if err != nil {
log.Fatal(err)
}
if content != "" {
if _, err := tmpfile.Write(c); err != nil {
log.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
} else {
if err := os.Remove(tmpfile.Name()); err != nil {
log.Fatal(err)
}
}
return tmpfile.Name()
}
func CreateConfigFile(content string, name string, path string) string {
if err := ioutil.WriteFile(path+"/"+name, []byte(content), 0600); err != nil {
log.Fatal(err)
}
return path + "/" + name
}

View File

@ -0,0 +1,8 @@
clair:
port: 6060
healthPort: 6061
uri: http://clair
priority: Low
report:
path: ./reports
format: html

View File

@ -0,0 +1,11 @@
CONTRIBUTION
-----------------
# Running full dev environnement
Update the configuration file `hyperclair.yml` with your clair container IP
```bash
# Running Authentication server, Registry, Clair & Postgres Clair db
docker-compose up -d
```

View File

@ -0,0 +1,29 @@
server:
addr: :5001
certificate: /ssl/server.pem
key: /ssl/server.key
token:
issuer: auth_service
expiration: 900
users:
"": {}
jgsqware:
password: $2y$05$oGKwJ8QJDLBOoTBmC/EQiefIMV1N9Yt9jpX3SqMoRqZRRql6q7yym
acl:
- match:
account: jgsqware
actions: ['*']
- match:
account: /.+/
name: ${account}/*
actions: ['*']
- match:
type: registry
name: catalog
actions: ['*']
comment: Anonymous users can get catalog.
- match:
account: ""
name: /.*/
actions: [pull]
comment: Anonymous users can pull everything.

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClbJbUWDCY31g5
hyFlhCpgXOntcqR72vVRew6vw9ooN7LKjC/ev1AbBD3JYca1R9vd/7D6wI5rYNR0
ZtCMxaQLEgvDf166VdrZewkq8UmbVSFwqtBvcZCG7mTapzgap6jD5H/fFoFoSkFa
V3dGWlEaS5emtk7fLjXTKO0OpByuO2mmRqAoTsnN4O3tYfrW7ReBHueibVIpjZo9
0qyFK3sZ2ewKG98m8OGh1jzOhxM8PZDKs9KjkZ/SJiGwxIQ/Ny+rXci7q3cshQna
n7ac2Wf0nG21j7eNk0lSKOkT3unb2+1B2+59mEtLauoYs1VcEmM55RnVdawvCmIy
LRowuLlDAgMBAAECggEAP3ELz1gbGyXcwgNPDY3Iarh3hncHGfD5UExvb30fN3lU
+lUVLsoUQKg5wffbqz5p1hPvndsnQ4sZL6MWrEZICW7cUBeTDsdKbUnAVFXBMu9N
KdZ4paTaFsVqrGihHafbE3WYjMgmzQZdVfZhafvNStZezLLyQKmKPvddItZRoYfN
sc+iFpT94hPp9Hjs9ClLQv/w9Xt8lVgD1FUh6yAlLUAn77HzbZuyC2nF4gbD2LiS
4G+xHcH77FyAU5W6BRv1DqNsuu0ksX/93GiYx0EebzT/IXa7xc0mYE0758EXk72y
yoznglkPkSOyyhcuI75FKMyYdQGKpyvw+y4aEv5JwQKBgQDTAaQ827Tpn/aMhP7L
jngFgTdfeq/7Q3eZjGgtr5RFnen6YS6WzWigvh5/70ASDziFd4fyd0P41/MjPkO6
FTFWisRCpW14+mSTUSDmgTQfsONy1Xr2ib4v4CX2NEy+nUsvpdl72dwZAG/fSu3K
MfkVksd5Z56WJ4wxKrB4riHukQKBgQDIsren8ljtxrLepMHvaNLx5Gl1EtrgX3gy
zTuUM2CSQktwBYNsp68AloOi6/tuH8t1xcBc8ywgCZ2WXzYdRL/koXsd2IpOTsLd
m/zGILgRPVow70yoxKxqxW8YYuQ1gLeAOshj8IHGGfnXTvvpNQNvrnja0NzavjFU
tR3aZQb8kwKBgQCOqNx2vQCKt7kEdmKiE1e4OQ3MAvH6SjoRWWmSAdSYYNSxkITk
NkpX61JJouNJknrfWdpTJymQk8hx+oXlyLBL15Qrjxb9pSTcqQw6a/5msryEhisV
hjlMuxpPZDrC4SvVMidhYgE58h6w9ELi4niKimtM/K6uzFwvXbJkVS7h0QKBgErT
Zum0zzcHZ9TedHfACzWoRTEi8HvK3FOEdPwSE6U0FlATniY6dmKvuzBY7wrly8OD
EO8WspLXQuu3X8OVyD2DfxVnkFkVwE1DRQDRXg7/YsrvzRL3EJlWNs9Ov2q7LK8g
O2oXVyr2sFF33y/ZVgijceeTC2R6mIXOaOzt0acFAoGASB7aF8PTT7YzFCGK/x2O
kg4GLJJSlDyhAZzQqe5LBZB+RhkoHZjdQHcMW84iHp8CsFqb3/D8X+5FsDkwBSMP
bN1fCFE03BsqubtKhI9kMz5hP1OhxlMZdMxRscbdRZqo57f3imtXg6laOktYyPOy
uOzr/Cxm5YUQqyAJ/S4zVuc=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAOMN706JOuJOMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTYwMTI4MTcwNjE4WhcNMTcwMTI3MTcwNjE4WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEApWyW1FgwmN9YOYchZYQqYFzp7XKke9r1UXsOr8PaKDeyyowv3r9QGwQ9
yWHGtUfb3f+w+sCOa2DUdGbQjMWkCxILw39eulXa2XsJKvFJm1UhcKrQb3GQhu5k
2qc4Gqeow+R/3xaBaEpBWld3RlpRGkuXprZO3y410yjtDqQcrjtppkagKE7JzeDt
7WH61u0XgR7nom1SKY2aPdKshSt7GdnsChvfJvDhodY8zocTPD2QyrPSo5Gf0iYh
sMSEPzcvq13Iu6t3LIUJ2p+2nNln9JxttY+3jZNJUijpE97p29vtQdvufZhLS2rq
GLNVXBJjOeUZ1XWsLwpiMi0aMLi5QwIDAQABo1AwTjAdBgNVHQ4EFgQUWCDpNrvl
IPntyV7Y4uyoAq+aPiQwHwYDVR0jBBgwFoAUWCDpNrvlIPntyV7Y4uyoAq+aPiQw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAROOTzaxkx9YdvnAEj8Pt
Ej1FgiN1ogxrL4RMwyXs5cAdHi0G71En9onB/8CaWXVrrJbu6DosNn2RQmNapxPG
CkT7QfuYVyZF5jtigsxc+W7aLzASLZHHRA0FcgxUAlEUVaxD3xs6U2jMMntp53ij
kOWmalMi5qOBps8PCD9sd9MDejLFihPAIz15l3TgVkbRvtcUlfmMio5AJYzjbm4/
0c8brR9tOp3qapeT78AhOmsF7zOVygd/BRIBG+Ynzo2DudBUs/j/4VOt9D9XO4I7
e3UaqN2OMcL5RYZ5cHemAAy9jjq9/NAYUyLLP0DiCe6OY7SKsDlGfkYVLpZMbUth
9w==
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDP8wLYKQgqxNRw
Liy4ZuYGHvDfK/NT0CIlvMJuobPt6NIGi6NRISLyR7eZvo6oHTN3i0ElpblzL0Hq
JmF87d7o7tqVxO2Fq+faslXrKTgMpDiPj+WxUR3igTy2+DZ4ZP00Y4jwaPYtvwSi
sOP2YeM6r+3sSETj5XonIr+/U3mQYkU9GgFQIbPmJnyPb+WaS0fBZ686zeIKvY3Y
+enSCKOjss1QFyTb0TmmwQUcTBCiEXV10FSgzV383ghj+Fq9+nKo3cwooHNYwU+a
/6gpI1GGWR074J7PGeBId4DNItcpR1x58BGmgaVHabsEW+9RdMNnh17QFk3Wt2eT
S5Knl4UTAgMBAAECggEAcmSoZ+kKiRyGEMAV8csJNszGjL5MuQqB/mh8PQfPR00Q
XHFsgjDMXKN/KKBfMbP+oACG8gLcpbSVeg1rC6J/QXxD2qfeUe5jOTdpdFfUcX/V
bYQnQwfwfK3DjJO2wzwq3irzJe1Xn4q5LhZJETyAF8S4CYcn/oY6UFUZTlLJSNcH
chQOFWvjk13DBjGAmZmjwWKxHoZsKs0ioHtShgONpPM8TZU6SmtJxdFD2pBNp+ba
Lj5IQUYWrfCudBlzqvpXmqBlZe1J1MG+FafvAKx2CFbYkVObjRa/5DtQs99qac8y
rhn8uloK9gljiszwwUVq/ImrUICP+20rHW+kLfHeYQKBgQD9U2XqXz0d2asD72wS
+6yhxY4KZ3TD6W3GgfADC/kTfY+pME7KAXr//7paJJP/GtOxsLGRDHV347c3o3js
OGlFWuUSsuJxGq4SwKuo9eRVbOMEXiVlgCuUL5HAk2co1MbKVhSJ5RGbrp6785JO
JJcuNUTlaUsgQExEsIFJmZpbdQKBgQDSJPwl3uZIg0GC4QbQTAG1ueiQ9oPJ9kyz
cjT31ar9L6VrLwY/MMHYKgBD5JLxkq8qL1h9ii/LJKjX7rX3yttu/qtTMO4Y82CP
XnmR5kbODUUfiirQjTQFS3YP390nAewLwRgYPcvpyNIWA6Im6UdFJECLOTUBeiYg
VumEhSe1ZwKBgAEj6faHHThQLYPkBQGE3n8P65bCZnUnTNYy6Yip+iILU6U4UXJ5
VTtnxEf5mCzyyvcmy3XSr4itnrqCYt31Vwv338YYxgoqS5RMB7nH+ZIk3lS7s8Fk
NU4CdM6AG1vEsWxhvM/uFwkzXQWNkCAH7CJKHRhHRA5OG8nHXZ2eMmKtAoGBAJ0J
1IA8fVys8bTrkprwYcq6/ifugHfZnmHvM9QNEXWZOIXLo2BvgDyYzo/t7T2nv0zI
Ctnt/V9SqvaKxeNB7g+ZMtC9XQC6R2t8T18PddQfqIs0RmCJVNmsFbMxOOQglJQI
HYhoDc1MLGsVFgT8CS2LNMyV2J2c+YbrTCCjHRR7AoGAICzoSClfvjmRg+4TP9/d
rixJF1UX77TnEhcHaFNBDnmSEX0K4rUr1o6GVZCwI+urL7ZmziDdmTDdbXWjqviJ
73COPw798Ox50VoVWssMGZQkXfbkk2yilLbok08ohlvVhzpiyecgbxAe4C6KRWWg
WEALyN3lILlyj1cYknRJ7gk=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDezCCAmOgAwIBAgIJAJXlshcLjIlpMA0GCSqGSIb3DQEBCwUAMFQxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxDTALBgNVBAMMBGF1dGgwHhcNMTYwMTI5MDcyMTE3WhcN
MTcwMTI4MDcyMTE3WjBUMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0
ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ0wCwYDVQQDDARh
dXRoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz/MC2CkIKsTUcC4s
uGbmBh7w3yvzU9AiJbzCbqGz7ejSBoujUSEi8ke3mb6OqB0zd4tBJaW5cy9B6iZh
fO3e6O7alcTthavn2rJV6yk4DKQ4j4/lsVEd4oE8tvg2eGT9NGOI8Gj2Lb8EorDj
9mHjOq/t7EhE4+V6JyK/v1N5kGJFPRoBUCGz5iZ8j2/lmktHwWevOs3iCr2N2Pnp
0gijo7LNUBck29E5psEFHEwQohF1ddBUoM1d/N4IY/havfpyqN3MKKBzWMFPmv+o
KSNRhlkdO+CezxngSHeAzSLXKUdcefARpoGlR2m7BFvvUXTDZ4de0BZN1rdnk0uS
p5eFEwIDAQABo1AwTjAdBgNVHQ4EFgQUcCD00y15Rdvwe8VnwoZee+J+6ucwHwYD
VR0jBBgwFoAUcCD00y15Rdvwe8VnwoZee+J+6ucwDAYDVR0TBAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAQEAvmlCA49FGGOZS5CWl/NzH3es3N1Gr8MihdAK0vYLxbOM
8qA2PirEjJ6sWSeB0ZthVpk/dcod68r4dpFh7hpypvaEerFbpr+eWa9nf/KVJ/ft
ClLw+iWZpjEjmtSbSg/XIfraWfvwQp9XNMcmIeHvovHd4HyyU1Ulx6aE31wnZ6SJ
UKhTPgft0DRsmvFMc683jjeUg/Ik/XknnCiSyfVvwv7UEUs7sH85mE0p4giJxhEv
7MdGlQkob+58BpzsErjoj+RpZSljna98NpwBZUfbxkYE2KzU0oqPC0zQ8KawPtw1
OB9O45KN2mJ9dPIAbezQHolrTQ7V+49/nhTghS/T3Q==
-----END CERTIFICATE-----

View File

@ -0,0 +1,77 @@
# 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:
# PostgreSQL Connection string
# http://www.postgresql.org/docs/9.4/static/libpq-connect.html
source: postgresql://postgres:root@postgres:5432?sslmode=disable
# 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
port: 6060
# Health server port
# This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server.
healthport: 6061
# Deadline before an API request will respond with a 503
timeout: 900s
# 32-bit URL-safe base64 key used to encrypt pagination tokens
# If one is not provided, it will be generated.
# Multiple clair instances in the same cluster need the same value.
paginationKey:
# 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: 0
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

@ -0,0 +1,54 @@
version: '2'
services:
auth:
image: cesanta/docker_auth:stable
ports:
- "5001:5001"
volumes:
- ./auth_server/config:/config:ro
- ./auth_server/ssl:/ssl
command: --v=2 --alsologtostderr /config/auth_config.yml
container_name: "auth"
registry:
image: registry:2.2.1
ports:
- 5000:5000
volumes:
- ./auth_server/ssl:/ssl
- registry-data:/var/lib/registry
container_name: "registry"
environment:
- REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/var/lib/registry
- REGISTRY_AUTH=token
- REGISTRY_AUTH_TOKEN_REALM=https://auth:5001/auth
- REGISTRY_AUTH_TOKEN_SERVICE="registry"
- REGISTRY_AUTH_TOKEN_ISSUER="auth_service"
- REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/ssl/server.pem
clair:
image: quay.io/coreos/clair
volumes:
- /tmp:/tmp
- ./config:/config
- clair-data:/var/local
ports:
- 6060:6060
- 6061:6061
container_name: "clair"
command: --log-level=debug --config=/config/clair.yml
postgres:
image: postgres
container_name: "postgres"
environment:
- POSTGRES_PASSWORD=root
volumes:
clair-data:
driver: local
hyperclair-data:
driver: local
registry-data:
driver: local

View File

@ -0,0 +1,32 @@
package docker
import (
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/api/v1"
"github.com/coreos/clair/cmd/clairclt/clair"
"github.com/coreos/clair/cmd/clairclt/xstrings"
)
//Analyse return Clair Image analysis
func Analyse(image Image) clair.ImageAnalysis {
c := len(image.FsLayers)
res := []v1.LayerEnvelope{}
for i := range image.FsLayers {
l := image.FsLayers[c-i-1].BlobSum
lShort := xstrings.Substr(l, 0, 12)
if a, err := clair.Analyse(l); err != nil {
logrus.Infof("analysing layer [%v] %d/%d: %v", lShort, i+1, c, err)
} else {
logrus.Infof("analysing layer [%v] %d/%d", lShort, i+1, c)
res = append(res, a)
}
}
return clair.ImageAnalysis{
Registry: xstrings.TrimPrefixSuffix(image.Registry, "http://", "/v2"),
ImageName: image.Name,
Tag: image.Tag,
Layers: res,
}
}

View File

@ -0,0 +1,90 @@
package docker
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/coreos/clair/cmd/clairclt/config"
"github.com/coreos/clair/cmd/clairclt/docker/httpclient"
)
var ErrUnauthorized = errors.New("unauthorized access")
type token struct {
Value string `json:"token"`
}
func (tok token) String() string {
return tok.Value
}
//BearerAuthParams parse Bearer Token on Www-Authenticate header
func BearerAuthParams(r *http.Response) map[string]string {
s := strings.Fields(r.Header.Get("Www-Authenticate"))
if len(s) != 2 || s[0] != "Bearer" {
return nil
}
result := map[string]string{}
for _, kv := range strings.Split(s[1], ",") {
parts := strings.Split(kv, "=")
if len(parts) != 2 {
continue
}
result[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ")
}
return result
}
func AuthenticateResponse(dockerResponse *http.Response, request *http.Request) error {
bearerToken := BearerAuthParams(dockerResponse)
url := bearerToken["realm"] + "?service=" + bearerToken["service"]
if bearerToken["scope"] != "" {
url += "&scope=" + bearerToken["scope"]
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
l, err := config.GetLogin(request.URL.Host)
if err != nil {
return err
}
req.SetBasicAuth(l.Username, l.Password)
response, err := httpclient.Get().Do(req)
if err != nil {
return err
}
if response.StatusCode == http.StatusUnauthorized {
return ErrUnauthorized
}
if response.StatusCode != http.StatusOK {
return fmt.Errorf("authentication server response: %v - %v", response.StatusCode, response.Status)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
}
var tok token
err = json.Unmarshal(body, &tok)
if err != nil {
return err
}
request.Header.Set("Authorization", "Bearer "+tok.String())
return nil
}

View File

@ -0,0 +1,24 @@
package httpclient
import (
"crypto/tls"
"net/http"
"github.com/spf13/viper"
)
var client *http.Client
//Get create a http.Client with Transport configuration
func Get() *http.Client {
if client == nil {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: viper.GetBool("auth.insecureSkipVerify")},
DisableCompression: true,
}
client = &http.Client{Transport: tr}
}
return client
}

View File

@ -0,0 +1,110 @@
package docker
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"github.com/spf13/viper"
)
var ErrDisallowed = errors.New("analysing official images is not allowed")
//Image represent Image Manifest from Docker image, including the registry URL
type Image struct {
Name string
Tag string
Registry string
FsLayers []Layer
}
//Layer represent the digest of a image layer
type Layer struct {
BlobSum string
History string
}
const dockerImageRegex = "^(?:([^/]+)/)?(?:([^/]+)/)?([^@:/]+)(?:[@:](.+))?"
const DockerHub = "registry-1.docker.io"
const hubURI = "https://" + DockerHub + "/v2"
var IsLocal = false
func TmpLocal() string {
return viper.GetString("hyperclair.tempFolder")
}
// Parse is used to parse a docker image command
//
//Example:
//"register.com:5080/wemanity-belgium/alpine"
//"register.com:5080/wemanity-belgium/alpine:latest"
//"register.com:5080/alpine"
//"register.com/wemanity-belgium/alpine"
//"register.com/alpine"
//"register.com/wemanity-belgium/alpine:latest"
//"alpine"
//"wemanity-belgium/alpine"
//"wemanity-belgium/alpine:latest"
func Parse(image string) (Image, error) {
imageRegex := regexp.MustCompile(dockerImageRegex)
if imageRegex.MatchString(image) == false {
return Image{}, fmt.Errorf("cannot parse image name: %v", image)
}
groups := imageRegex.FindStringSubmatch(image)
registry, repository, name, tag := groups[1], groups[2], groups[3], groups[4]
if tag == "" {
tag = "latest"
}
if repository == "" && !strings.ContainsAny(registry, ":.") {
repository, registry = registry, hubURI //Regex problem, if no registry in url, regex parse repository as registry, so need to invert it
} else {
//FIXME We need to move to https. <error: tls: oversized record received with length 20527>
//Maybe using a `insecure-registry` flag in configuration
if strings.Contains(registry, "docker") {
registry = "https://" + registry + "/v2"
} else {
registry = "http://" + registry + "/v2"
}
}
if repository != "" {
name = repository + "/" + name
}
if strings.Contains(registry, "docker.io") && repository == "" {
return Image{}, ErrDisallowed
}
return Image{
Registry: registry,
Name: name,
Tag: tag,
}, nil
}
// BlobsURI run Blobs URI as <registry>/<imageName>/blobs/<digest>
// eg: "http://registry:5000/v2/jgsqware/ubuntu-git/blobs/sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e"
func (image Image) BlobsURI(digest string) string {
return strings.Join([]string{image.Registry, image.Name, "blobs", digest}, "/")
}
func (image Image) String() string {
return image.Registry + "/" + image.Name + ":" + image.Tag
}
func (image Image) AsJSON() (string, error) {
b, err := json.Marshal(image)
if err != nil {
return "", fmt.Errorf("cannot marshal image: %v", err)
}
return string(b), nil
}

View File

@ -0,0 +1,74 @@
package docker
import "testing"
var imageNameTests = []struct {
in string
out string
}{
{"jgsqware/ubuntu-git", hubURI + "/jgsqware/ubuntu-git:latest"},
{"wemanity-belgium/registry-backup", hubURI + "/wemanity-belgium/registry-backup:latest"},
{"wemanity-belgium/alpine:latest", hubURI + "/wemanity-belgium/alpine:latest"},
{"register.com/alpine", "http://register.com/v2/alpine:latest"},
{"register.com/wemanity-belgium/alpine", "http://register.com/v2/wemanity-belgium/alpine:latest"},
{"register.com/wemanity-belgium/alpine:latest", "http://register.com/v2/wemanity-belgium/alpine:latest"},
{"register.com:5080/alpine", "http://register.com:5080/v2/alpine:latest"},
{"register.com:5080/wemanity-belgium/alpine", "http://register.com:5080/v2/wemanity-belgium/alpine:latest"},
{"register.com:5080/wemanity-belgium/alpine:latest", "http://register.com:5080/v2/wemanity-belgium/alpine:latest"},
{"registry:5000/google/cadvisor", "http://registry:5000/v2/google/cadvisor:latest"},
}
var invalidImageNameTests = []struct {
in string
out string
}{
{"alpine", hubURI + "/alpine:latest"},
{"docker.io/golang", hubURI + "/golang:latest"},
}
func TestParse(t *testing.T) {
for _, imageName := range imageNameTests {
image, err := Parse(imageName.in)
if err != nil {
t.Errorf("Parse(\"%s\") should be valid: %v", imageName.in, err)
}
if image.String() != imageName.out {
t.Errorf("Parse(\"%s\") => %v, want %v", imageName.in, image, imageName.out)
}
}
}
func TestParseDisallowed(t *testing.T) {
for _, imageName := range invalidImageNameTests {
_, err := Parse(imageName.in)
if err != ErrDisallowed {
t.Errorf("Parse(\"%s\") should failed with err \"%v\": %v", imageName.in, ErrDisallowed, err)
}
}
}
func TestMBlobstURI(t *testing.T) {
image, err := Parse("localhost:5000/alpine")
if err != nil {
t.Error(err)
}
result := image.BlobsURI("sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e")
if result != "http://localhost:5000/v2/alpine/blobs/sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e" {
t.Errorf("Is %s, should be http://localhost:5000/v2/alpine/blobs/sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e", result)
}
}
func TestUniqueLayer(t *testing.T) {
image := Image{
FsLayers: []Layer{Layer{BlobSum: "test1"}, Layer{BlobSum: "test1"}, Layer{BlobSum: "test2"}},
}
image.uniqueLayers()
if len(image.FsLayers) > 2 {
t.Errorf("Layers must be unique: %v", image.FsLayers)
}
}

View File

@ -0,0 +1,171 @@
package docker
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"github.com/Sirupsen/logrus"
)
//Prepare populate image.FSLayers with the layer from manifest coming from `docker save` command. Layer.History will be populated with `docker history` command
func Prepare(im *Image) error {
imageName := im.Name + ":" + im.Tag
logrus.Debugf("preparing %v", imageName)
path, err := save(imageName)
// defer os.RemoveAll(path)
if err != nil {
return fmt.Errorf("could not save image: %s", err)
}
// Retrieve history.
logrus.Infoln("Getting image's history")
manifestLayerIDs, err := historyFromManifest(path)
historyLayerIDs, err := historyFromCommand(imageName)
if err != nil || (len(manifestLayerIDs) == 0 && len(historyLayerIDs) == 0) {
return fmt.Errorf("Could not get image's history: %s", err)
}
for i, l := range manifestLayerIDs {
im.FsLayers = append(im.FsLayers, Layer{BlobSum: l, History: historyLayerIDs[i]})
}
return nil
}
//FromHistory populate image.FSLayers with the layer from `docker history` command
func FromHistory(im *Image) error {
imageName := im.Name + ":" + im.Tag
layerIDs, err := historyFromCommand(imageName)
if err != nil || len(layerIDs) == 0 {
return fmt.Errorf("Could not get image's history: %s", err)
}
for _, l := range layerIDs {
im.FsLayers = append(im.FsLayers, Layer{BlobSum: l})
}
return nil
}
func cleanLocal() error {
logrus.Debugln("cleaning temporary local repository")
err := os.RemoveAll(TmpLocal())
if err != nil {
return fmt.Errorf("cleaning temporary local repository: %v", err)
}
return nil
}
func save(imageName string) (string, error) {
path := TmpLocal() + "/" + strings.Split(imageName, ":")[0] + "/blobs"
if _, err := os.Stat(path); os.IsExist(err) {
err := os.RemoveAll(path)
if err != nil {
return "", err
}
}
err := os.MkdirAll(path, 0755)
if err != nil {
return "", err
}
var stderr bytes.Buffer
logrus.Debugln("docker image to save: ", imageName)
logrus.Debugln("saving in: ", path)
save := exec.Command("docker", "save", imageName)
save.Stderr = &stderr
extract := exec.Command("tar", "xf", "-", "-C"+path)
extract.Stderr = &stderr
pipe, err := extract.StdinPipe()
if err != nil {
return "", err
}
save.Stdout = pipe
err = extract.Start()
if err != nil {
return "", errors.New(stderr.String())
}
err = save.Run()
if err != nil {
return "", errors.New(stderr.String())
}
err = pipe.Close()
if err != nil {
return "", err
}
err = extract.Wait()
if err != nil {
return "", errors.New(stderr.String())
}
return path, nil
}
func historyFromManifest(path string) ([]string, error) {
mf, err := os.Open(path + "/manifest.json")
if err != nil {
return nil, err
}
defer mf.Close()
// https://github.com/docker/docker/blob/master/image/tarexport/tarexport.go#L17
type manifestItem struct {
Config string
RepoTags []string
Layers []string
}
var manifest []manifestItem
if err = json.NewDecoder(mf).Decode(&manifest); err != nil {
return nil, err
} else if len(manifest) != 1 {
return nil, err
}
var layers []string
for _, layer := range manifest[0].Layers {
layers = append(layers, strings.TrimSuffix(layer, "/layer.tar"))
}
return layers, nil
}
func historyFromCommand(imageName string) ([]string, error) {
var stderr bytes.Buffer
cmd := exec.Command("docker", "history", "-q", "--no-trunc", imageName)
cmd.Stderr = &stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
return []string{}, err
}
err = cmd.Start()
if err != nil {
return []string{}, errors.New(stderr.String())
}
var layers []string
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
layers = append(layers, scanner.Text())
}
for i := len(layers)/2 - 1; i >= 0; i-- {
opp := len(layers) - 1 - i
layers[i], layers[opp] = layers[opp], layers[i]
}
return layers, nil
}

View File

@ -0,0 +1,46 @@
package docker
import (
"fmt"
"net/http"
"strings"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/cmd/clairclt/docker/httpclient"
)
//Pull Image from Registry or Hub depending on image name
func Login(registry string) (bool, error) {
logrus.Info("log in: ", registry)
if strings.Contains(registry, "docker") {
registry = "https://" + registry + "/v2"
} else {
registry = "http://" + registry + "/v2"
}
client := httpclient.Get()
request, err := http.NewRequest("GET", registry, nil)
response, err := client.Do(request)
if err != nil {
return false, fmt.Errorf("log in %v: %v", registry, err)
}
authorized := response.StatusCode != http.StatusUnauthorized
if !authorized {
logrus.Info("Unauthorized access")
err := AuthenticateResponse(response, request)
if err != nil {
if err == ErrUnauthorized {
authorized = false
}
return false, err
} else {
authorized = true
}
}
return authorized, nil
}

View File

@ -0,0 +1,90 @@
package docker
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/cmd/clairclt/docker/httpclient"
)
var ErrNotFound = errors.New("image not found")
//Pull Image from Registry or Hub depending on image name
func Pull(imageName string) (Image, error) {
image, err := Parse(imageName)
if err != nil {
return Image{}, err
}
logrus.Info("pulling image: ", image)
mURI := fmt.Sprintf("%v/%v/manifests/%v", image.Registry, image.Name, image.Tag)
client := httpclient.Get()
request, err := http.NewRequest("GET", mURI, nil)
response, err := client.Do(request)
if err != nil {
return Image{}, fmt.Errorf("retrieving manifest: %v", err)
}
if response.StatusCode == http.StatusUnauthorized {
logrus.Info("Pull is Unauthorized")
err := AuthenticateResponse(response, request)
if err != nil {
return Image{}, fmt.Errorf("authenticating: %v", err)
}
response, err = client.Do(request)
if err != nil {
return Image{}, fmt.Errorf("retrieving manifest: %v", err)
}
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return Image{}, fmt.Errorf("reading manifest body: %v", err)
}
if response.StatusCode != 200 {
switch response.StatusCode {
case http.StatusUnauthorized:
return Image{}, ErrUnauthorized
case http.StatusNotFound:
return Image{}, ErrNotFound
default:
return Image{}, fmt.Errorf("%d - %s", response.StatusCode, string(body))
}
}
if err := image.parseManifest(body); err != nil {
return Image{}, fmt.Errorf("parsing manifest: %v", err)
}
return image, nil
}
func (image *Image) parseManifest(body []byte) error {
err := json.Unmarshal(body, &image)
if err != nil {
return fmt.Errorf("unmarshalling manifest body: %v", err)
}
image.uniqueLayers()
return nil
}
func (image *Image) uniqueLayers() {
encountered := map[Layer]bool{}
result := []Layer{}
for index := range image.FsLayers {
if encountered[image.FsLayers[index]] != true {
encountered[image.FsLayers[index]] = true
result = append(result, image.FsLayers[index])
}
}
image.FsLayers = result
}

View File

@ -0,0 +1,87 @@
package docker
import (
"fmt"
"strings"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/api/v1"
"github.com/coreos/clair/cmd/clairclt/clair"
"github.com/coreos/clair/cmd/clairclt/config"
"github.com/coreos/clair/cmd/clairclt/xstrings"
)
var registryMapping map[string]string
//Push image to Clair for analysis
func Push(image Image) error {
layerCount := len(image.FsLayers)
parentID := ""
if layerCount == 0 {
logrus.Warningln("there is no layer to push")
}
localIP, err := config.LocalServerIP()
if err != nil {
return err
}
hURL := fmt.Sprintf("http://%v/v2", localIP)
if IsLocal {
hURL = strings.Replace(hURL, "/v2", "/local", -1)
logrus.Infof("using %v as local url", hURL)
}
for index, layer := range image.FsLayers {
lUID := xstrings.Substr(layer.BlobSum, 0, 12)
logrus.Infof("Pushing Layer %d/%d [%v]", index+1, layerCount, lUID)
insertRegistryMapping(layer.BlobSum, image.Registry)
payload := v1.LayerEnvelope{Layer: &v1.Layer{
Name: layer.BlobSum,
Path: image.BlobsURI(layer.BlobSum),
ParentName: parentID,
Format: "Docker",
}}
//FIXME Update to TLS
if IsLocal {
payload.Layer.Name = layer.History
payload.Layer.Path += "/layer.tar"
}
payload.Layer.Path = strings.Replace(payload.Layer.Path, image.Registry, hURL, 1)
if err := clair.Push(payload); err != nil {
logrus.Infof("adding layer %d/%d [%v]: %v", index+1, layerCount, lUID, err)
if err != clair.ErrOSNotSupported {
return err
}
parentID = ""
} else {
parentID = payload.Layer.Name
}
}
if IsLocal {
if err := cleanLocal(); err != nil {
return err
}
}
return nil
}
func insertRegistryMapping(layerDigest string, registryURI string) {
logrus.Debugf("Saving %s[%s]", layerDigest, registryURI)
registryMapping[layerDigest] = registryURI
}
//GetRegistryMapping return the registryURI corresponding to the layerID passed as parameter
func GetRegistryMapping(layerDigest string) (string, error) {
registryURI, present := registryMapping[layerDigest]
if !present {
return "", fmt.Errorf("%v mapping not found", layerDigest)
}
return registryURI, nil
}
func init() {
registryMapping = map[string]string{}
}

View File

@ -0,0 +1,28 @@
package docker
import "testing"
func TestInsertRegistryMapping(t *testing.T) {
layerID := "sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e"
registryURI := "registry:5000"
insertRegistryMapping(layerID, registryURI)
if r := registryMapping[layerID]; r != registryURI {
t.Errorf("insertRegistryMapping(%q,%q) => %q, want %q", layerID, registryURI, r, registryURI)
}
}
func TestGetRegistryMapping(t *testing.T) {
layerID := "sha256:13be4a52fdee2f6c44948b99b5b65ec703b1ca76c1ab5d2d90ae9bf18347082e"
registryURI := "registry:5000"
insertRegistryMapping(layerID, registryURI)
if r, err := GetRegistryMapping(layerID); r != registryURI {
if err != nil {
t.Errorf("GetRegistryMapping(%q) failed => %v", layerID, err)
} else {
t.Errorf("GetRegistryMapping(%q) => %q, want %q", layerID, registryURI, r)
}
}
}

View File

@ -0,0 +1,10 @@
clair:
port: 6060
healthPort: 6061
uri: http://clair
priority: Low
report:
path: ./reports
format: html
hyperclair:
interface: virtualbox

View File

@ -0,0 +1,8 @@
clair:
port: 6060
healthPort: 6061
uri: http://clair
priority: Low
report:
path: ./reports
format: html

21
cmd/clairclt/main.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright © 2016 NAME HERE <EMAIL ADDRESS>
//
// 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.
package main
import "github.com/coreos/clair/cmd/clairclt/cmd"
func main() {
cmd.Execute()
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,99 @@
package server
import (
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"regexp"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/coreos/clair/cmd/clairclt/docker"
"github.com/coreos/clair/cmd/clairclt/docker/httpclient"
"github.com/spf13/viper"
)
//Serve run a local server with the fileserver and the reverse proxy
func Serve(sURL string) error {
go func() {
http.Handle("/v2/", newSingleHostReverseProxy())
http.Handle("/local/", http.StripPrefix("/local", restrictedFileServer(docker.TmpLocal())))
listener := tcpListener(sURL)
logrus.Info("Starting Server on ", listener.Addr())
if err := http.Serve(listener, nil); err != nil {
logrus.Fatalf("local server error: %v", err)
}
}()
//sleep needed to wait the server start. Maybe use a channel for that
time.Sleep(5 * time.Millisecond)
return nil
}
func tcpListener(sURL string) (listener net.Listener) {
listener, err := net.Listen("tcp", sURL)
if err != nil {
logrus.Fatalf("cannot instanciate listener: %v", err)
}
if viper.GetInt("hyperclair.port") == 0 {
port := strings.Split(listener.Addr().String(), ":")[1]
logrus.Debugf("Update local server port from %q to %q", "0", port)
viper.Set("hyperclair.port", port)
}
return
}
func restrictedFileServer(path string) http.Handler {
if _, err := os.Stat(path); os.IsNotExist(err) {
os.Mkdir(path, 0777)
}
fc := func(w http.ResponseWriter, r *http.Request) {
http.FileServer(http.Dir(path)).ServeHTTP(w, r)
}
return http.HandlerFunc(fc)
}
func newSingleHostReverseProxy() *httputil.ReverseProxy {
director := func(request *http.Request) {
var validID = regexp.MustCompile(`.*/blobs/(.*)$`)
u := request.URL.Path
logrus.Debugf("request url: %v", u)
if !validID.MatchString(u) {
logrus.Errorf("cannot parse url: %v", u)
}
host, _ := docker.GetRegistryMapping(validID.FindStringSubmatch(u)[1])
out, _ := url.Parse(host)
request.URL.Scheme = out.Scheme
request.URL.Host = out.Host
client := httpclient.Get()
req, _ := http.NewRequest("HEAD", request.URL.String(), nil)
resp, err := client.Do(req)
if err != nil {
logrus.Errorf("response error: %v", err)
return
}
if resp.StatusCode == http.StatusUnauthorized {
logrus.Info("pull from clair is unauthorized")
docker.AuthenticateResponse(resp, request)
}
r, _ := http.NewRequest("GET", request.URL.String(), nil)
r.Header.Set("Authorization", request.Header.Get("Authorization"))
r.Header.Set("Accept-Encoding", request.Header.Get("Accept-Encoding"))
*request = *r
}
return &httputil.ReverseProxy{
Director: director,
}
}

View File

@ -0,0 +1,30 @@
package xstrings
import (
"encoding/json"
"strings"
)
//Substr extract string of length in s starting at pos
func Substr(s string, pos, length int) string {
runes := []rune(s)
l := pos + length
if l > len(runes) {
l = len(runes)
}
return string(runes[pos:l])
}
//TrimPrefixSuffix combine TrimPrefix and TrimSuffix
func TrimPrefixSuffix(s string, prefix string, suffix string) string {
return strings.TrimSuffix(strings.TrimPrefix(s, prefix), suffix)
}
//ToIndentJSON apply json.MarshalIndent with tabulation on an interface and return the formatted string
func ToIndentJSON(v interface{}) ([]byte, error) {
b, err := json.MarshalIndent(v, "", "\t")
if err != nil {
return nil, err
}
return b, nil
}

View File

@ -0,0 +1,27 @@
package xstrings
import "testing"
func TestSubstrFromBeginning(t *testing.T) {
commitID := "e3ff9321271b0a5cec45ca6e0cdc72b2f376afd2"
expected := "e3ff9"
if s := Substr(commitID, 0, 5); s != expected {
t.Errorf("is %v, expect %v", s, expected)
}
}
func TestSubstrFromCharFive(t *testing.T) {
commitID := "e3ff9321271b0a5cec45ca6e0cdc72b2f376afd2"
expected := "32127"
if s := Substr(commitID, 5, 5); s != expected {
t.Errorf("is %v, expect %v", s, expected)
}
}
func TestTrimPrefixSuffix(t *testing.T) {
v := "http://registry:5555/v2"
e := "registry:5555"
if s := TrimPrefixSuffix(v, "http://", "/v2"); s != e {
t.Errorf("is %v, expect %v", s, e)
}
}