diff --git a/internal/icinga2/icinga.go b/internal/icinga2/icinga.go index 8d95914..cdb78c4 100644 --- a/internal/icinga2/icinga.go +++ b/internal/icinga2/icinga.go @@ -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) @@ -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 { diff --git a/internal/icinga2/options.go b/internal/icinga2/options.go new file mode 100644 index 0000000..c38d68b --- /dev/null +++ b/internal/icinga2/options.go @@ -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 + }) +} diff --git a/main.go b/main.go index 1d6bbe6..237204a 100644 --- a/main.go +++ b/main.go @@ -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")