Skip to content

Commit

Permalink
config: move notifier structs into this package
Browse files Browse the repository at this point in the history
Doing this to facilitate breaking this package into its own module.

This requires changing the Validate method so that it doesn't modify
internal state, as it will be unable to be accessed.

Signed-off-by: Hank Donnay <hdonnay@redhat.com>
  • Loading branch information
hdonnay committed Nov 3, 2021
1 parent 0439169 commit 11509ee
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 0 deletions.
178 changes: 178 additions & 0 deletions config/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package config

import (
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/quay/clair/v4/notifier/amqp"
Expand Down Expand Up @@ -89,3 +92,178 @@ func (n *Notifier) Validate(combo bool) error {
}
return nil
}

type Webhook struct {
// any HTTP headers necessary for the request to Target
Headers http.Header `yaml:"headers" json:"headers"`
// the URL where our webhook will be delivered
Target string `yaml:"target" json:"target"`
// the callback url where notifications can be received
// the notification will be appended to this url
Callback string `yaml:"callback" json:"callback"`
// whether the webhook deliverer will sign out going.
// if true webhooks will be sent with a jwt signed by
// the notifier's private key.
Signed bool `yaml:"signed" json:"signed"`
}

// Validate will return a copy of the Config on success.
// If any validation fails an error will be returned.
func (c *Webhook) Validate() error {
if _, err := url.Parse(c.Target); err != nil {
return fmt.Errorf("failed to parse target url")
}

// Require trailing slash so url.Parse() can easily append notification id.
if !strings.HasSuffix(c.Callback, "/") {
c.Callback = c.Callback + "/"
}

if _, err := url.Parse(c.Callback); err != nil {
return fmt.Errorf("failed to parse callback url: %w", err)
}

return nil
}

// Exchange are the required fields necessary to check
// the existence of an Exchange
//
// For more details see: https://godoc.org/github.com/streadway/amqp#Channel.ExchangeDeclarePassive
type Exchange struct {
// The name of the exchange
Name string `yaml:"name" json:"name"`
// The type of the exchange. Typically:
// "direct"
// "fanout"
// "topic"
// "headers"
Type string `yaml:"type" json:"type"`
// Whether the exchange survives server restarts
Durable bool `yaml:"durability" json:"durability"`
// Whether bound consumers define the lifecycle of the Exchange.
AutoDelete bool `yaml:"auto_delete" json:"auto_delete"`
}

// AMQP provides configuration for an AMQP deliverer.
type AMQP struct {
TLS *TLS `yaml:"tls" json:"tls"`
// The AMQP exchange notifications will be delivered to.
// A passive declare is performed and if the exchange does not exist
// the declare will fail.
Exchange Exchange `yaml:"exchange" json:"exchange"`
// The routing key used to route notifications to the desired queue.
RoutingKey string `yaml:"routing_key" json:"routing_key"`
// The callback url where notifications are retrieved.
Callback string `yaml:"callback" json:"callback"`
// A list of AMQP compliant URI scheme. see: https://www.rabbitmq.com/uri-spec.html
// example: "amqp://user:pass@host:10000/vhost"
//
// The first successful connection will be used by the amqp deliverer.
//
// If "amqps://" broker URI schemas are provided the TLS configuration below is required.
URIs []string `yaml:"uris" json:"uris"`
// Specifies the number of notifications delivered in single AMQP message
// when Direct is true.
//
// Ignored if Direct is not true
// If 0 or 1 is provided no rollup occurs and each notification is delivered
// separately.
Rollup int `yaml:"rollup" json:"rollup"`
// AMQPConfigures the AMQP delivery to deliver notifications directly to
// the configured Exchange.
//
// If true "Callback" is ignored.
// If false a notifier.Callback is delivered to the queue and clients
// utilize the pagination API to retrieve.
Direct bool `yaml:"direct" json:"direct"`
}

// Validate confirms configuration is valid.
func (c *AMQP) Validate() error {
if c.Exchange.Type == "" {
return fmt.Errorf("AMQP config requires the exchange.type field")
}
if c.RoutingKey == "" {
return fmt.Errorf("AMQP config requires the routing key field")
}
for _, uri := range c.URIs {
if strings.HasPrefix(uri, "amqps://") {
if c.TLS.RootCA == "" {
return fmt.Errorf("amqps:// broker requires tls_root_ca")
}
if c.TLS.Cert == "" {
return fmt.Errorf("amqps:// broker requires tls_cert")
}
if c.TLS.Key == "" {
return fmt.Errorf("amqps:// broker requires tls_key")
}
}
}

if c.TLS != nil {
if c.TLS.Cert == "" || c.TLS.Key == "" {
return fmt.Errorf("both tls cert and key are required")
}
}

if !c.Direct {
if !strings.HasSuffix(c.Callback, "/") {
c.Callback = c.Callback + "/"
}
if _, err := url.Parse(c.Callback); err != nil {
return fmt.Errorf("failed to parse callback url: %w", err)
}
}
return nil
}

type Login struct {
Login string `yaml:"login" json:"login"`
Passcode string `yaml:"passcode" json:"passcode"`
}

type STOMP struct {
// optional tls portion of config
TLS *TLS `yaml:"tls" json:"tls"`
// optional user login portion of config
Login *Login `yaml:"user" json:"user"`
// The callback url where notifications are retrieved.
Callback string `yaml:"callback" json:"callback"`
// the destination messages will be delivered to
Destination string `yaml:"destination" json:"destination"`
// a list of URIs to send messages to.
// a linear search of this list is always performed.
URIs []string `yaml:"uris" json:"uris"`
// Specifies the number of notifications delivered in single STOMP message
// when Direct is true.
//
// Ignored if Direct is not true
// If 0 or 1 is provided no rollup occurs and each notification is delivered
// separately.
Rollup int `yaml:"rollup" json:"rollup"`
// Configures the STOMP delivery to deliver notifications directly to
// the configured Destination.
//
// If true "Callback" is ignored.
// If false a notifier.Callback is delivered to the queue and clients
// utilize the pagination API to retrieve.
Direct bool `yaml:"direct" json:"direct"`
}

func (c *STOMP) Validate() error {
if !c.Direct {
if !strings.HasSuffix(c.Callback, "/") {
c.Callback = c.Callback + "/"
}
if _, err := url.Parse(c.Callback); err != nil {
return fmt.Errorf("failed to parse callback url: %w", err)
}
}
if c.TLS != nil {
if c.TLS.Cert == "" || c.TLS.Key == "" {
return fmt.Errorf("both tls cert and key are required")
}
}
return nil
}
58 changes: 58 additions & 0 deletions config/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package config

import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
)

type TLS struct {
// The filesystem path where a root CA can be read.
//
// This can also be controlled by the SSL_CERT_FILE and SSL_CERT_DIR
// environment variables, or adding the relevant certs to the system trust
// store.
RootCA string `yaml:"root_ca" json:"root_ca"`
// The filesystem path where a tls certificate can be read.
Cert string `yaml:"cert" json:"cert"`
// The filesystem path where a tls private key can be read.
Key string `yaml:"key" json:"key"`
}

// Config returns a tls.Config modified according to the TLS struct.
//
// If the *TLS is nil, a default tls.Config is returned.
func (t *TLS) Config() (*tls.Config, error) {
var cfg tls.Config
if t == nil {
return &cfg, nil
}

if t.Cert == "" || t.Key == "" {
return nil, errors.New("both tls cert and key are required")
}
if t.RootCA != "" {
p, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
ca, err := os.ReadFile(t.RootCA)
if err != nil {
return nil, fmt.Errorf("failed to read tls root ca: %w", err)
}
if !p.AppendCertsFromPEM(ca) {
return nil, errors.New("unable to add certificate to pool")
}
cfg.RootCAs = p
}

cert, err := tls.LoadX509KeyPair(t.Cert, t.Key)
if err != nil {
return nil, fmt.Errorf("failed to read x509 cert and key pair: %w", err)
}
cfg.Certificates = append(cfg.Certificates, cert)

return &cfg, nil
}

0 comments on commit 11509ee

Please sign in to comment.