Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 51 additions & 28 deletions internal/icinga2/icinga.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,46 +34,75 @@ var (
// Client is what we use to talk to the Icinga2 API
type Client struct {
IcingaURL []string
Username string
Password string
headers map[string]string
httpClient *http.Client
logger *slog.Logger
config *config.Config
}

func NewClient(config *config.Config, logger *slog.Logger) *Client {
var rt http.RoundTripper = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
DisableKeepAlives: config.IcingaDisableKeepAlives,
return NewClientWithOptions(
WithIcingaBasicAuth(config.IcingaUser, config.IcingaPassword),
WithIcingaAPIEndpoints(config.IcingaURL),
WithIcingaLogger(logger),
WithTransportDisableKeepAlives(config.IcingaDisableKeepAlives),
WithTransportTLSClientConfig(config.IcingaTLSConfig),
)
}

// NewClientWithOptions returns a [Client] instance using the builder pattern.
// the provided options affect various internal components of the result.
func NewClientWithOptions(opts ...builderOption) *Client {
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 30 * time.Second,
TLSClientConfig: config.IcingaTLSConfig,
}

client := &http.Client{
// We could also use a context.WithTimeout for the requests
Timeout: 10 * time.Second,
Transport: rt,
Timeout: 10 * time.Second,
}
headers := map[string]string{
HeaderAccept: MIMETypeApplicationJSON,
HeaderContentType: MIMETypeApplicationJSON,
}
icinga := &Client{
IcingaURL: []string{},
headers: headers,
logger: slog.Default(),
}

for _, o := range opts {
switch f := o.(type) {
case clientOption:
f.ApplyClient(client)
case dialerOption:
f.ApplyDialer(dialer)
case icingaOption:
f.ApplyIcinga(icinga)
case transportOption:
f.ApplyTransport(transport)
}
}

return &Client{
IcingaURL: config.IcingaURL,
Username: config.IcingaUser,
Password: config.IcingaPassword,
httpClient: client,
config: config,
logger: logger,
}
transport.DialContext = dialer.DialContext
client.Transport = transport
icinga.httpClient = client

return icinga
}

// Do is a small wrapper that tries the given request against all given Icinga API endpoints
func (c *Client) Do(req *http.Request, path string) (*http.Response, error) {
var endpointErrors strings.Builder

for k, v := range c.headers {
req.Header.Set(k, v)
}

for _, base := range c.IcingaURL {
// Since we determine what the path ist, this probably won't fail
req.URL, _ = req.URL.Parse(base + path)
Expand All @@ -86,12 +115,6 @@ func (c *Client) Do(req *http.Request, path string) (*http.Response, error) {

c.logger.Debug(fmt.Sprintf("Calling Icinga API at %s", req.URL), "component", "icinga")

req.Header.Set("User-Agent", "alertmanager-icinga-bridge")
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")

req.SetBasicAuth(c.Username, c.Password)

resp, err := c.httpClient.Do(req)

if err != nil {
Expand Down
215 changes: 215 additions & 0 deletions internal/icinga2/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
//nolint:revive,ireturn
package icinga2

import (
"crypto/tls"
"encoding/base64"
"log/slog"
"net"
"net/http"
"time"
)

const (
scopeClient = iota
scopeDialer
scopeIcinga
scopeTransport
)

const (
HeaderAccept = "Accept"
HeaderAuthorization = "Authorization"
HeaderContentType = "Content-Type"
HeaderUserAgent = "UserAgent"
)

const (
MIMETypeApplicationJSON = "application/json"
)

// internal marker for builders to use in argument list
type builderOption interface {
Scope() int
}

// clientOption is a builder option acting on [http.Client] instances
type clientOption interface {
builderOption

// ApplyClient applies its internal logic onto the provided instance
ApplyClient(v *http.Client)
}

// clientOptionFunc is a [clientOption] implementation
type clientOptionFunc func(*http.Client)

// Scope implements [builderOption] and returns [scopeClient]
func (f clientOptionFunc) Scope() int {
return scopeClient
}

// ApplyClient implements [clientOption] by calling f(v)
func (f clientOptionFunc) ApplyClient(v *http.Client) {
f(v)
}

// WithClientTimeout sets the client timeout to the provided value
func WithClientTimeout(d time.Duration) clientOption {
return clientOptionFunc(func(c *http.Client) {
c.Timeout = d
})
}

// dialerOption is a builder option acting on [net.Dialer] instances
type dialerOption interface {
builderOption

// ApplyDialer applies its internal logic onto the provided instance
ApplyDialer(v *net.Dialer)
}

// dialerOptionFunc is a [dialerOption] implementation
type dialerOptionFunc func(*net.Dialer)

// Scope implements [builderOption] and returns [scopeDialer]
func (f dialerOptionFunc) Scope() int {
return scopeDialer
}

// ApplyDialer implements [dialerOption] by calling f(v)
func (f dialerOptionFunc) ApplyDialer(v *net.Dialer) {
f(v)
}

// WithDialerTimeout sets the dialer timeout to the provided value
func WithDialerTimeout(d time.Duration) dialerOption {
return dialerOptionFunc(func(v *net.Dialer) {
v.Timeout = d
})
}

// WithDialerKeepAlive sets the KeepAlive flag on the dialer
func WithDialerKeepAlive(d time.Duration) dialerOption {
return dialerOptionFunc(func(v *net.Dialer) {
v.KeepAlive = d
})
}

// icingaOption is a builder option acting on [Client] instances
type icingaOption interface {
builderOption

// ApplyIcinga applies its internal logic onto the provided instance
ApplyIcinga(v *Client)
}

// icingaOptionFunc is a [icingaOption] implementation
type icingaOptionFunc func(*Client)

// Scope implements [builderOption] and returns [scopeIcinga]
func (f icingaOptionFunc) Scope() int {
return scopeIcinga
}

// ApplyIcinga implements [icingaOption] by calling f(v)
func (f icingaOptionFunc) ApplyIcinga(v *Client) {
f(v)
}

// WithIcingaBasicAuth adds an authorization headers using the provided
// credentials with a "Basic" scheme
func WithIcingaBasicAuth(username, password string) icingaOption {
auth := username + ":" + password
token := base64.StdEncoding.EncodeToString([]byte(auth))

return icingaOptionFunc(func(v *Client) {
v.headers[HeaderAuthorization] = "Basic " + token
})
}

// WithIcingaUserAgent adds a user-agent header using the provided information
func WithIcingaUserAgent(application, version, metadata string) icingaOption {
ident := application + "/" + version + " (" + metadata + ")"

return icingaOptionFunc(func(v *Client) {
v.headers[HeaderUserAgent] = ident
})
}

// WithIcingaHeader add a key/value pair as header to outgoing requests
func WithIcingaHeader(key, value string) icingaOption {
return icingaOptionFunc(func(v *Client) {
v.headers[key] = value
})
}

// WithIcingaAPIEndpoint adds the URL to the list of remote endpoints to connect to
func WithIcingaAPIEndpoint(url string) icingaOption {
return icingaOptionFunc(func(v *Client) {
v.IcingaURL = append(v.IcingaURL, url)
})
}

// WithIcingaAPIEndpoints adds the URLs to the list of remote endpoints to connect to
func WithIcingaAPIEndpoints(urls []string) icingaOption {
return icingaOptionFunc(func(v *Client) {
v.IcingaURL = append(v.IcingaURL, urls...)
})
}

// WithIcingaLogger changes the internal logger to the provided instance
func WithIcingaLogger(l *slog.Logger) icingaOption {
return icingaOptionFunc(func(v *Client) {
v.logger = l
})
}

// transportOption is a builder option acting on [http.Transport] instances
type transportOption interface {
builderOption

// ApplyTransport applies its internal logic onto the provided instance
ApplyTransport(v *http.Transport)
}

// transportOptionFunc is a [transportOption] implementation
type transportOptionFunc func(*http.Transport)

// Scope implements [builderOption] and returns [scopeTransport]
func (f transportOptionFunc) Scope() int {
return scopeTransport
}

// ApplyTransport implements [transportOption] by calling f(v)
func (f transportOptionFunc) ApplyTransport(v *http.Transport) {
f(v)
}

// WithTransportDisableKeepAlives sets the DisableKeepAlives flag on the transport instance
func WithTransportDisableKeepAlives(b bool) transportOption {
return transportOptionFunc(func(v *http.Transport) {
v.DisableKeepAlives = b
})
}

// WithTransportForceAttemptHTTP2 sets the ForceAttemptHTTP2 flag on the transport instance
func WithTransportForceAttemptHTTP2(b bool) transportOption {
return transportOptionFunc(func(v *http.Transport) {
v.ForceAttemptHTTP2 = b
})
}

// WithTransportTLSHandshakeTimeout sets the TLS handshake timeout on the transport instance
func WithTransportTLSHandshakeTimeout(d time.Duration) transportOption {
return transportOptionFunc(func(v *http.Transport) {
v.TLSHandshakeTimeout = d
})
}

// WithTransportTLSClientConfig sets the TLS configuration on the transport instance
func WithTransportTLSClientConfig(c *tls.Config) transportOption {
return transportOptionFunc(func(v *http.Transport) {
v.TLSClientConfig = c
})
}
9 changes: 8 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,14 @@ func main() {
defer cancel()

// Create Icinga Client
icingaClient := icinga2.NewClient(cfg, logger)
icingaClient := icinga2.NewClientWithOptions(
icinga2.WithIcingaBasicAuth(cfg.IcingaUser, cfg.IcingaPassword),
icinga2.WithIcingaAPIEndpoints(cfg.IcingaURL),
icinga2.WithIcingaLogger(logger),
icinga2.WithTransportDisableKeepAlives(cfg.IcingaDisableKeepAlives),
icinga2.WithTransportTLSClientConfig(cfg.IcingaTLSConfig),
icinga2.WithIcingaUserAgent("alertmanager-icinga-bridge", version, commit),
)

logger.Info("Starting alertmanager-icinga-bridge", "version", version, "commit", commit, "component", "main")

Expand Down