265 lines
7.1 KiB
Go
265 lines
7.1 KiB
Go
|
// 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.
|
||
|
|
||
|
// Package mysql implements database.Datastore with MySQL.
|
||
|
package mysql
|
||
|
|
||
|
import (
|
||
|
"database/sql"
|
||
|
"fmt"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"path"
|
||
|
"runtime"
|
||
|
"strings"
|
||
|
|
||
|
"bitbucket.org/liamstask/goose/lib/goose"
|
||
|
"github.com/coreos/clair/config"
|
||
|
"github.com/coreos/clair/database"
|
||
|
cerrors "github.com/coreos/clair/utils/errors"
|
||
|
"github.com/coreos/pkg/capnslog"
|
||
|
_ "github.com/go-sql-driver/mysql"
|
||
|
"github.com/hashicorp/golang-lru"
|
||
|
"github.com/pborman/uuid"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
log = capnslog.NewPackageLogger("github.com/coreos/clair", "mysql")
|
||
|
)
|
||
|
|
||
|
const DATABASENAME = "clair"
|
||
|
const DEFAULTFLAG = "?charset=utf8&parseTime=True"
|
||
|
const DEFAULTSOURCE = DATABASENAME + DEFAULTFLAG
|
||
|
|
||
|
type Queryer interface {
|
||
|
Query(query string, args ...interface{}) (*sql.Rows, error)
|
||
|
QueryRow(query string, args ...interface{}) *sql.Row
|
||
|
Exec(query string, args ...interface{}) (sql.Result, error)
|
||
|
}
|
||
|
|
||
|
type mySQL struct {
|
||
|
*sql.DB
|
||
|
cache *lru.ARCCache
|
||
|
}
|
||
|
|
||
|
func (mySQL *mySQL) Close() {
|
||
|
mySQL.DB.Close()
|
||
|
}
|
||
|
|
||
|
func (mySQL *mySQL) Ping() bool {
|
||
|
return mySQL.DB.Ping() == nil
|
||
|
}
|
||
|
|
||
|
// Open creates a Datastore backed by a PostgreSQL database.
|
||
|
// It will run immediately every necessary migration on the database.
|
||
|
func Open(config *config.DatabaseConfig) (database.Datastore, error) {
|
||
|
source := config.Source
|
||
|
if strings.HasPrefix(source, "mysql://") {
|
||
|
source = strings.TrimPrefix(source, "mysql://")
|
||
|
}
|
||
|
config.Source = source + DEFAULTSOURCE
|
||
|
// Create Database if not exists
|
||
|
err := createDatabase(source, DATABASENAME)
|
||
|
if err != nil {
|
||
|
log.Error(err)
|
||
|
return nil, database.ErrCantOpen
|
||
|
}
|
||
|
return open(config)
|
||
|
}
|
||
|
|
||
|
func open(config *config.DatabaseConfig) (database.Datastore, error) {
|
||
|
// Run migrations.
|
||
|
if err := migrate(config.Source); err != nil {
|
||
|
log.Error(err)
|
||
|
return nil, database.ErrCantOpen
|
||
|
}
|
||
|
|
||
|
// Open database.
|
||
|
db, err := sql.Open("mysql", config.Source)
|
||
|
if err != nil {
|
||
|
log.Error(err)
|
||
|
return nil, database.ErrCantOpen
|
||
|
}
|
||
|
|
||
|
// Initialize cache.
|
||
|
// TODO(Quentin-M): Benchmark with a simple LRU Cache.
|
||
|
var cache *lru.ARCCache
|
||
|
if config.CacheSize > 0 {
|
||
|
cache, _ = lru.NewARC(config.CacheSize)
|
||
|
}
|
||
|
|
||
|
return &mySQL{DB: db, cache: cache}, nil
|
||
|
}
|
||
|
|
||
|
// migrate runs all available migrations on a pgSQL database.
|
||
|
func migrate(dataSource string) error {
|
||
|
log.Info("running database migrations")
|
||
|
|
||
|
_, filename, _, _ := runtime.Caller(1)
|
||
|
migrationDir := path.Join(path.Dir(filename), "/migrations/")
|
||
|
conf := &goose.DBConf{
|
||
|
MigrationsDir: migrationDir,
|
||
|
Driver: goose.DBDriver{
|
||
|
Name: "mysql",
|
||
|
OpenStr: dataSource,
|
||
|
Import: "github.com/go-sql-driver/mysql",
|
||
|
Dialect: &goose.MySqlDialect{},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
// Determine the most recent revision available from the migrations folder.
|
||
|
target, err := goose.GetMostRecentDBVersion(conf.MigrationsDir)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Run migrations
|
||
|
err = goose.RunMigrations(conf, conf.MigrationsDir, target)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
log.Info("database migration ran successfully")
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// TODO:
|
||
|
// createDatabase creates a new database.
|
||
|
// The dataSource parameter should not contain a dbname.
|
||
|
func createDatabase(dataSource, databaseName string) error {
|
||
|
// Open database.
|
||
|
log.Info("Create database: ", databaseName)
|
||
|
db, err := sql.Open("mysql", dataSource)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("could not open database (CreateDatabase): %v", err)
|
||
|
}
|
||
|
defer db.Close()
|
||
|
|
||
|
// Create database.
|
||
|
_, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + databaseName + " DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci")
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("could not create database: %v", err)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// dropDatabase drops an existing database.
|
||
|
// The dataSource parameter should not contain a dbname.
|
||
|
func dropDatabase(dataSource, databaseName string) error {
|
||
|
// Open database.
|
||
|
db, err := sql.Open("mysql", dataSource)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("could not open database (DropDatabase): %v", err)
|
||
|
}
|
||
|
defer db.Close()
|
||
|
|
||
|
// Drop database.
|
||
|
if _, err = db.Exec("DROP DATABASE " + databaseName); err != nil {
|
||
|
return fmt.Errorf("could not drop database: %v", err)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// TODO
|
||
|
// pgSQLTest wraps pgSQL for testing purposes.
|
||
|
// Its Close() method drops the database.
|
||
|
type pgSQLTest struct {
|
||
|
*mySQL
|
||
|
dataSourceDefaultDatabase string
|
||
|
dbName string
|
||
|
}
|
||
|
|
||
|
// OpenForTest creates a test Datastore backed by a new PostgreSQL database.
|
||
|
// It creates a new unique and prefixed ("test_") database.
|
||
|
// Using Close() will drop the database.
|
||
|
func OpenForTest(name string, withTestData bool) (*pgSQLTest, error) {
|
||
|
// Define the PostgreSQL connection strings.
|
||
|
dataSource := "root@tcp(127.0.0.1:3306)/"
|
||
|
if dataSourceEnv := os.Getenv("CLAIR_TEST_MYSQL"); dataSourceEnv != "" {
|
||
|
dataSource = dataSourceEnv
|
||
|
}
|
||
|
dbName := "test_" + strings.ToLower(name) + "_" + strings.Replace(uuid.New(), "-", "_", -1)
|
||
|
dataSourceDefaultDatabase := dataSource
|
||
|
dataSourceTestDatabase := dataSource + dbName + "?charset=utf8&parseTime=True"
|
||
|
|
||
|
// Create database.
|
||
|
if err := createDatabase(dataSourceDefaultDatabase, dbName); err != nil {
|
||
|
log.Error(err)
|
||
|
return nil, database.ErrCantOpen
|
||
|
}
|
||
|
|
||
|
// Open database.
|
||
|
db, err := open(&config.DatabaseConfig{Source: dataSourceTestDatabase, CacheSize: 0})
|
||
|
if err != nil {
|
||
|
dropDatabase(dataSourceDefaultDatabase, dbName)
|
||
|
log.Error(err)
|
||
|
return nil, database.ErrCantOpen
|
||
|
}
|
||
|
// Load test data if specified.
|
||
|
if withTestData {
|
||
|
_, filename, _, _ := runtime.Caller(0)
|
||
|
d, _ := ioutil.ReadFile(path.Join(path.Dir(filename)) + "/testdata/data.sql")
|
||
|
queries := strings.Split(fmt.Sprintf("%s", d), ";")
|
||
|
for _, q := range queries {
|
||
|
_, err = db.(*mySQL).Exec(q)
|
||
|
if err != nil {
|
||
|
dropDatabase(dataSourceDefaultDatabase, dbName)
|
||
|
log.Error(err)
|
||
|
return nil, database.ErrCantOpen
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return &pgSQLTest{
|
||
|
mySQL: db.(*mySQL),
|
||
|
dataSourceDefaultDatabase: dataSourceDefaultDatabase,
|
||
|
dbName: dbName}, nil
|
||
|
}
|
||
|
|
||
|
func (pgSQL *pgSQLTest) Close() {
|
||
|
pgSQL.DB.Close()
|
||
|
dropDatabase(pgSQL.dataSourceDefaultDatabase, pgSQL.dbName)
|
||
|
}
|
||
|
|
||
|
// handleError logs an error with an extra description and masks the error if it's an SQL one.
|
||
|
// This ensures we never return plain SQL errors and leak anything.
|
||
|
func handleError(desc string, err error) error {
|
||
|
if err == nil {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
if err == sql.ErrNoRows {
|
||
|
return cerrors.ErrNotFound
|
||
|
}
|
||
|
|
||
|
log.Errorf("%s: %v", desc, err)
|
||
|
database.PromErrorsTotal.WithLabelValues(desc).Inc()
|
||
|
|
||
|
if err == sql.ErrTxDone || strings.HasPrefix(err.Error(), "sql:") {
|
||
|
return database.ErrBackendException
|
||
|
}
|
||
|
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// isErrUniqueViolation determines is the given error is a duplicate entry error.
|
||
|
func isErrUniqueViolation(err error) bool {
|
||
|
if strings.Contains(fmt.Sprintf("%v", err), "Error 1062") {
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|