Skip to content

Commit

Permalink
config: add multiple database configuration
Browse files Browse the repository at this point in the history
This will give us the ability to add additional database backends if the
need arises.

Signed-off-by: Hank Donnay <hdonnay@redhat.com>
  • Loading branch information
hdonnay committed Jul 6, 2023
1 parent c28648e commit c34db4e
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 29 deletions.
69 changes: 67 additions & 2 deletions config/database.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package config

import (
"fmt"
"net/url"
"os"
"strings"
)

func checkDSN(s string) (w []Warning, err error) {
var errConnString = Warning{
path: ".connstring",
inner: fmt.Errorf(`using bare-string for database configuration deprecated: %w`, ErrDeprecated),
}

func checkPostgresqlDSN(s string) (w []Warning) {
switch {
case s == "":
// Nothing specified, make sure something's in the environment.
Expand Down Expand Up @@ -38,5 +44,64 @@ func checkDSN(s string) (w []Warning, err error) {
msg: "unable to make sense of connection string",
})
}
return w, nil
return w
}

// Database indicates the database configuration.
type Database struct {
// Name indicates which database backend to use.
//
// This value must match the json/yaml tag.
Name string `json:"name" yaml:"name"`
// Migrations indicates if database migrations should run automatically.
Migrations *bool `json:"migrations,omitempty" yaml:"migrations,omitempty"`
// PostgreSQL is the PostgreSQL configuration.
PostgreSQL *DatabasePostgreSQL `json:"postgresql,omitempty" yaml:"postgresql,omitempty"`
}

var (
_ linter = (*Database)(nil)
_ validator = (*Database)(nil)
)

func (d *Database) lint() (ws []Warning, err error) {
switch n := d.Name; n {
case "postgresql": // OK
case "postgres":
ws = append(ws, Warning{
msg: fmt.Sprintf("unknown database: %q (did you mean %q?)", n, "postgresql"),
path: ".name",
})
default:
ws = append(ws, Warning{
msg: fmt.Sprintf("unknown database: %q", n),
path: ".name",
})
}
return ws, nil
}
func (d *Database) validate(_ Mode) ([]Warning, error) {
return d.lint()
}

// DatabasePostgreSQL is the PostgreSQL-specific database configuration.
type DatabasePostgreSQL struct {
// DSN is a data source name (aka "connection string") as documented for
// [libpq], with the extensions supported by [pgxpool].
//
// [libpq]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
// [pgxpool]: https://pkg.go.dev/github.com/jackc/pgx/v4/pgxpool#ParseConfig
DSN string `json:"dsn" yaml:"dsn"`
}

var (
_ linter = (*DatabasePostgreSQL)(nil)
_ validator = (*DatabasePostgreSQL)(nil)
)

func (d *DatabasePostgreSQL) lint() ([]Warning, error) {
return checkPostgresqlDSN(d.DSN), nil
}
func (d *DatabasePostgreSQL) validate(_ Mode) ([]Warning, error) {
return d.lint()
}
40 changes: 40 additions & 0 deletions config/database_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package config

import (
"encoding/json"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestDatabaseUnmarshal(t *testing.T) {
want := Database{
Name: "postgresql",
PostgreSQL: &DatabasePostgreSQL{
DSN: "host=test",
},
}
input := []string{
`{"name":"postgresql","postgresql":{"dsn":"host=test"}}`,
}

for _, tc := range input {
t.Logf("testing: %#q", tc)
var got Database
if err := json.Unmarshal([]byte(tc), &got); err != nil {
t.Error(err)
continue
}
ws, err := got.lint()
if err != nil {
t.Error(err)
continue
}
for _, w := range ws {
t.Logf("got lint: %v", &w)
}
if !cmp.Equal(&got, &want) {
t.Error(cmp.Diff(&got, &want))
}
}
}
33 changes: 26 additions & 7 deletions config/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ type Indexer struct {
// url: "postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full"
// or
// string: "user=pqgotest dbname=pqgotest sslmode=verify-full"
ConnString string `yaml:"connstring" json:"connstring"`
//
// Deprecated: Use the ".database" member instead.
ConnString string `yaml:"connstring,omitempty" json:"connstring,omitempty"`
// Database is the database configuration.
Database *Database `yaml:"database,omitempty" json:"database,omitempty"`
// A positive value representing seconds.
//
// Concurrent Indexers lock on manifest scans to avoid clobbering.
Expand All @@ -34,7 +38,9 @@ type Indexer struct {
// A "true" or "false" value
//
// Whether Indexer nodes handle migrations to their database.
Migrations bool `yaml:"migrations,omitempty" json:"migrations,omitempty"`
//
// Deprecated: Use the ".database.migrations" member instead.
Migrations *bool `yaml:"migrations,omitempty" json:"migrations,omitempty"`
// Airgap disables HTTP access to the Internet. This affects both indexers and
// the layer fetcher. Database connections are unaffected.
//
Expand Down Expand Up @@ -66,17 +72,30 @@ func (i *Indexer) validate(mode Mode) (ws []Warning, err error) {
msg: `automatically sizing number of concurrent requests`,
})
}
if i.ConnString != "" {
ws = append(ws, errConnString)
i.ConnString = ""
if d := i.Database; d != nil {
d.Name = `postgresql`
d.PostgreSQL = &DatabasePostgreSQL{
DSN: i.ConnString,
}
d.Migrations = i.Migrations
}
}
lws, err := i.lint()
return append(ws, lws...), err
}

func (i *Indexer) lint() (ws []Warning, err error) {
ws, err = checkDSN(i.ConnString)
if err != nil {
return ws, err
if i.ConnString != "" {
ws = append(ws, errConnString)
}
for i := range ws {
ws[i].path = ".connstring"
if i.Database == nil {
ws = append(ws, Warning{
path: ".database",
msg: `missing database configuration`,
})
}
if i.ScanLockRetry > 10 { // Guess at what a "large" value is here.
ws = append(ws, Warning{
Expand Down
6 changes: 3 additions & 3 deletions config/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ func ExampleLint() {
// error: <nil>
// warning: http listen address not provided, default will be used (at $.http_listen_addr)
// warning: introspection address not provided, default will be used (at $.introspection_addr)
// warning: connection string is empty and no relevant environment variables found (at $.indexer.connstring)
// warning: connection string is empty and no relevant environment variables found (at $.matcher.connstring)
// warning: missing database configuration (at $.indexer.database)
// warning: missing database configuration (at $.matcher.database)
// warning: updater period is very aggressive: most sources are updated daily (at $.matcher.period)
// warning: update garbage collection is off (at $.matcher.update_retention)
// warning: connection string is empty and no relevant environment variables found (at $.notifier.connstring)
// warning: missing database configuration (at $.notifier.database)
// warning: interval is very fast: may result in increased workload (at $.notifier.poll_interval)
// warning: interval is very fast: may result in increased workload (at $.notifier.delivery_interval)
}
38 changes: 29 additions & 9 deletions config/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ type Matcher struct {
// url: "postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full"
// or
// string: "user=pqgotest dbname=pqgotest sslmode=verify-full"
//
// Deprecated: Use the ".database" member instead.
ConnString string `yaml:"connstring" json:"connstring"`
// Database is the database configuration.
Database *Database `yaml:"database,omitempty" json:"database,omitempty"`
// A string in <host>:<port> format where <host> can be an empty string.
//
// A Matcher contacts an Indexer to create a VulnerabilityReport.
Expand All @@ -36,7 +40,7 @@ type Matcher struct {
// Clair allows for a custom connection pool size. This number will
// directly set how many active sql connections are allowed concurrently.
//
// Deprecated: Pool size should be set through the ConnString member.
// Deprecated: Pool size should be set through the database configuration.
// Currently, Clair only uses the "pgxpool" package to connect to the
// database, so see
// https://pkg.go.dev/github.com/jackc/pgx/v4/pgxpool#ParseConfig for more
Expand All @@ -51,15 +55,17 @@ type Matcher struct {
// A "true" or "false" value
//
// Whether Matcher nodes handle migrations to their databases.
Migrations bool `yaml:"migrations,omitempty" json:"migrations,omitempty"`
//
// Deprecated: Use the ".database.migrations" member instead.
Migrations *bool `yaml:"migrations,omitempty" json:"migrations,omitempty"`
// DisableUpdaters disables the updater's running of matchers.
//
// This should be toggled on if vulnerabilities are being provided by
// another mechanism.
DisableUpdaters bool `yaml:"disable_updaters,omitempty" json:"disable_updaters,omitempty"`
}

func (m *Matcher) validate(mode Mode) ([]Warning, error) {
func (m *Matcher) validate(mode Mode) (ws []Warning, err error) {
if mode != ComboMode && mode != MatcherMode {
return nil, nil
}
Expand Down Expand Up @@ -90,16 +96,30 @@ func (m *Matcher) validate(mode Mode) ([]Warning, error) {
default:
panic("programmer error")
}
return m.lint()
if m.ConnString != "" {
ws = append(ws, errConnString)
m.ConnString = ""
if d := m.Database; d != nil {
d.Name = `postgresql`
d.PostgreSQL = &DatabasePostgreSQL{
DSN: m.ConnString,
}
d.Migrations = m.Migrations
}
}
lws, err := m.lint()
return append(ws, lws...), err
}

func (m *Matcher) lint() (ws []Warning, err error) {
ws, err = checkDSN(m.ConnString)
if err != nil {
return ws, err
if m.ConnString != "" {
ws = append(ws, errConnString)
}
for i := range ws {
ws[i].path = ".connstring"
if m.Database == nil {
ws = append(ws, Warning{
path: ".database",
msg: `missing database configuration`,
})
}

if m.Period < Duration(DefaultMatcherPeriod) {
Expand Down
37 changes: 29 additions & 8 deletions config/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ type Notifier struct {
// url: "postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full"
// or
// string: "user=pqgotest dbname=pqgotest sslmode=verify-full"
//
// Deprecated: Use the ".database" member instead.
ConnString string `yaml:"connstring" json:"connstring"`
// Database is the database configuration.
Database *Database `yaml:"database,omitempty" json:"database,omitempty"`
// A string in <host>:<port> format where <host> can be an empty string.
//
// A Notifier contacts an Indexer to create obtain manifests affected by vulnerabilities.
Expand Down Expand Up @@ -63,10 +67,12 @@ type Notifier struct {
// A "true" or "false" value
//
// Whether Notifier nodes handle migrations to their database.
Migrations bool `yaml:"migrations,omitempty" json:"migrations,omitempty"`
//
// Deprecated: Use the ".database.migrations" member instead.
Migrations *bool `yaml:"migrations,omitempty" json:"migrations,omitempty"`
}

func (n *Notifier) validate(mode Mode) ([]Warning, error) {
func (n *Notifier) validate(mode Mode) (ws []Warning, err error) {
if mode != ComboMode && mode != NotifierMode {
return nil, nil
}
Expand All @@ -88,17 +94,32 @@ func (n *Notifier) validate(mode Mode) ([]Warning, error) {
default:
panic("programmer error")
}
return n.lint()
if n.ConnString != "" {
ws = append(ws, errConnString)
n.ConnString = ""
if d := n.Database; d != nil {
d.Name = `postgresql`
d.PostgreSQL = &DatabasePostgreSQL{
DSN: n.ConnString,
}
d.Migrations = n.Migrations
}
}
lws, err := n.lint()
return append(ws, lws...), err
}

func (n *Notifier) lint() (ws []Warning, err error) {
ws, err = checkDSN(n.ConnString)
if err != nil {
return ws, err
if n.ConnString != "" {
ws = append(ws, errConnString)
}
for i := range ws {
ws[i].path = ".connstring"
if n.Database == nil {
ws = append(ws, Warning{
path: ".database",
msg: `missing database configuration`,
})
}

got := 0
if n.AMQP != nil {
got++
Expand Down

0 comments on commit c34db4e

Please sign in to comment.