Skip to content
Merged
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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ build-backend:
start-backend:
go run ./cmd/plugin-backend.go -port='9001' -config-path='./config' -static-path='./web/dist'

.PHONY: test-backend
test-backend:
go test ./pkg/... -v

.PHONY: build-image
build-image:
./scripts/build-image.sh
Expand Down
89 changes: 88 additions & 1 deletion cmd/plugin-backend.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"context"
"crypto/tls"
"flag"
"os"
"strconv"
Expand All @@ -21,6 +23,9 @@ var (
logLevelArg = flag.String("log-level", logrus.InfoLevel.String(), "verbosity of logs\noptions: ['panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace']\n'trace' level will log all incoming requests\n(default 'error')")
alertmanagerUrlArg = flag.String("alertmanager", "", "alertmanager url to proxy to for acm mode")
thanosQuerierUrlArg = flag.String("thanos-querier", "", "thanos querier url to proxy to for acm mode")
tlsMinVersionArg = flag.String("tls-min-version", "", "minimum TLS version\noptions: ['VersionTLS10', 'VersionTLS11', 'VersionTLS12', 'VersionTLS13']\n(default 'VersionTLS12')")
tlsMaxVersionArg = flag.String("tls-max-version", "", "maximum TLS version\noptions: ['VersionTLS10', 'VersionTLS11', 'VersionTLS12', 'VersionTLS13']\n(default is the highest supported by Go)")
tlsCipherSuitesArg = flag.String("tls-cipher-suites", "", "comma-separated list of cipher suites for the server\nvalues are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants)")
log = logrus.WithField("module", "main")
)

Expand All @@ -37,6 +42,9 @@ func main() {
logLevel := mergeEnvValue("MONITORING_PLUGIN_LOG_LEVEL", *logLevelArg, logrus.InfoLevel.String())
alertmanagerUrl := mergeEnvValue("MONITORING_PLUGIN_ALERTMANAGER", *alertmanagerUrlArg, "")
thanosQuerierUrl := mergeEnvValue("MONITORING_PLUGIN_THANOS_QUERIER", *thanosQuerierUrlArg, "")
tlsMinVersion := mergeEnvValue("TLS_MIN_VERSION", *tlsMinVersionArg, "")
tlsMaxVersion := mergeEnvValue("TLS_MAX_VERSION", *tlsMaxVersionArg, "")
tlsCipherSuites := mergeEnvValue("TLS_CIPHER_SUITES", *tlsCipherSuitesArg, "")

featuresList := strings.Fields(strings.Join(strings.Split(strings.ToLower(features), ","), " "))

Expand All @@ -54,7 +62,12 @@ func main() {

log.Infof("enabled features: %+q\n", featuresList)

server.Start(&server.Config{
// Parse TLS configuration
tlsMinVer := parseTLSVersion(tlsMinVersion)
tlsMaxVer := parseTLSVersion(tlsMaxVersion)
tlsCiphers := parseCipherSuites(tlsCipherSuites)

srv, err := server.CreateServer(context.Background(), &server.Config{
Port: port,
CertFile: cert,
PrivateKeyFile: key,
Expand All @@ -64,7 +77,18 @@ func main() {
PluginConfigPath: pluginConfigPath,
AlertmanagerUrl: alertmanagerUrl,
ThanosQuerierUrl: thanosQuerierUrl,
TLSMinVersion: tlsMinVer,
TLSMaxVersion: tlsMaxVer,
TLSCipherSuites: tlsCiphers,
})

if err != nil {
panic(err)
}

if err = srv.StartHTTPServer(); err != nil {
panic(err)
}
}

func mergeEnvValue(key string, arg string, defaultValue string) string {
Expand Down Expand Up @@ -95,3 +119,66 @@ func mergeEnvValueInt(key string, arg int, defaultValue int) int {

return defaultValue
}

func getCipherSuitesMap() map[string]uint16 {
result := make(map[string]uint16)

for _, suite := range tls.CipherSuites() {
result[suite.Name] = suite.ID
}

return result
}

func getTLSVersionsMap() map[string]uint16 {
versions := make(map[string]uint16)

versions["VersionTLS12"] = tls.VersionTLS12
versions["VersionTLS13"] = tls.VersionTLS13

return versions
}

func parseTLSVersion(version string) uint16 {
if version == "" {
return tls.VersionTLS12
}

tlsVersions := getTLSVersionsMap()

if v, ok := tlsVersions[version]; ok {
return v
}

log.Warnf("Invalid TLS version %q, using default VersionTLS12", version)
return tls.VersionTLS12
}

func parseCipherSuites(ciphers string) []uint16 {
if ciphers == "" {
return nil
}

cipherMap := getCipherSuitesMap()

cipherNames := strings.Split(strings.ReplaceAll(ciphers, " ", ""), ",")
var result []uint16

for _, name := range cipherNames {
if name == "" {
continue
}
if cipher, ok := cipherMap[name]; ok {
result = append(result, cipher)
} else {
log.Warnf("Unknown cipher suite %q, skipping", name)
}
}

if len(result) == 0 {
log.Warn("No valid cipher suites provided, using Go defaults")
return nil
}

return result
}
121 changes: 92 additions & 29 deletions pkg/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ type Config struct {
PluginConfigPath string
AlertmanagerUrl string
ThanosQuerierUrl string
TLSMinVersion uint16
TLSMaxVersion uint16
TLSCipherSuites []uint16
}

func (c *Config) IsTLSEnabled() bool {
return c.CertFile != "" && c.PrivateKeyFile != ""
}

type PluginServer struct {
*http.Server
Config *Config
}

type PluginConfig struct {
Expand All @@ -61,19 +73,47 @@ func (pluginConfig *PluginConfig) MarshalJSON() ([]byte, error) {
})
}

func Start(cfg *Config) {
func CreateServer(ctx context.Context, cfg *Config) (*PluginServer, error) {
httpServer, err := createHTTPServer(ctx, cfg)
if err != nil {
return nil, err
}

return &PluginServer{
Config: cfg,
Server: httpServer,
}, nil
}

func (s *PluginServer) StartHTTPServer() error {
if s.Config.IsTLSEnabled() {
log.Infof("listening for https on %s", s.Server.Addr)
return s.Server.ListenAndServeTLS(s.Config.CertFile, s.Config.PrivateKeyFile)
}
log.Infof("listening for http on %s", s.Server.Addr)
return s.Server.ListenAndServe()
}

func (s *PluginServer) Shutdown(ctx context.Context) error {
if s.Server != nil {
return s.Server.Shutdown(ctx)
}
return nil
}

func createHTTPServer(ctx context.Context, cfg *Config) (*http.Server, error) {
acmMode := cfg.Features[AcmAlerting]
acmLocationsLength := len(cfg.AlertmanagerUrl) + len(cfg.ThanosQuerierUrl)

if acmLocationsLength > 0 && !acmMode {
log.Panic("alertmanager and thanos-querier cannot be set without the 'acm-alerting' feature flag")
return nil, fmt.Errorf("alertmanager and thanos-querier cannot be set without the 'acm-alerting' feature flag")
}
if acmLocationsLength == 0 && acmMode {
log.Panic("alertmanager and thanos-querier must be set to use the 'acm-alerting' feature flag")
return nil, fmt.Errorf("alertmanager and thanos-querier must be set to use the 'acm-alerting' feature flag")
}

if cfg.Port == int(proxy.AlertmanagerPort) || cfg.Port == int(proxy.ThanosQuerierPort) {
log.Panic(fmt.Printf("Cannot set default port to reserved port %d", cfg.Port))
return nil, fmt.Errorf("cannot set default port to reserved port %d", cfg.Port)
}

// Uncomment the following line for local development:
Expand All @@ -86,12 +126,12 @@ func Start(cfg *Config) {
k8sconfig, err := rest.InClusterConfig()

if err != nil {
panic(fmt.Errorf("cannot get in cluster config: %w", err))
return nil, fmt.Errorf("cannot get in cluster config: %w", err)
}

k8sclient, err = dynamic.NewForConfig(k8sconfig)
if err != nil {
panic(fmt.Errorf("error creating dynamicClient: %w", err))
return nil, fmt.Errorf("error creating dynamicClient: %w", err)
}
} else {
k8sclient = nil
Expand All @@ -100,15 +140,27 @@ func Start(cfg *Config) {
router, pluginConfig := setupRoutes(cfg)
router.Use(corsHeaderMiddleware())

tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
tlsEnabled := cfg.CertFile != "" && cfg.PrivateKeyFile != ""
tlsConfig := &tls.Config{}

tlsEnabled := cfg.IsTLSEnabled()
if tlsEnabled {
// Set MinVersion - default to TLS 1.2 if not specified
if cfg.TLSMinVersion != 0 {
tlsConfig.MinVersion = cfg.TLSMinVersion
} else {
tlsConfig.MinVersion = tls.VersionTLS12
}

if cfg.TLSMaxVersion != 0 {
tlsConfig.MaxVersion = cfg.TLSMaxVersion
}

if len(cfg.TLSCipherSuites) > 0 {
tlsConfig.CipherSuites = cfg.TLSCipherSuites
}

// Build and run the controller which reloads the certificate and key
// files whenever they change.
ctx := context.Background()

certKeyPair, err := dynamiccertificates.NewDynamicServingContentFromFiles("serving-cert", cfg.CertFile, cfg.PrivateKeyFile)
if err != nil {
log.WithError(err).Fatal("unable to create TLS controller")
Expand Down Expand Up @@ -138,6 +190,7 @@ func Start(cfg *Config) {
// Notify cert/key file changes to the controller.
certKeyPair.AddListener(ctrl)

// Start certificate controllers in background
go ctrl.Run(1, ctx.Done())
go certKeyPair.Run(ctx, 1)
}
Expand All @@ -160,18 +213,13 @@ func Start(cfg *Config) {
httpServer.Handler = loggedRouter
}

if tlsEnabled {
if acmMode {
startProxy(cfg, k8sclient, tlsConfig, timeout, proxy.AlertManagerKind, proxy.AlertmanagerPort)
startProxy(cfg, k8sclient, tlsConfig, timeout, proxy.ThanosQuerierKind, proxy.ThanosQuerierPort)
}

log.Infof("listening for https on %s", httpServer.Addr)
panic(httpServer.ListenAndServeTLS(cfg.CertFile, cfg.PrivateKeyFile))
} else {
log.Infof("listening for http on %s", httpServer.Addr)
panic(httpServer.ListenAndServe())
// Start proxy servers if in ACM mode
if tlsEnabled && acmMode {
startProxy(cfg, k8sclient, tlsConfig, timeout, proxy.AlertManagerKind, proxy.AlertmanagerPort)
startProxy(cfg, k8sclient, tlsConfig, timeout, proxy.ThanosQuerierKind, proxy.ThanosQuerierPort)
}

return httpServer, nil
}

func setupRoutes(cfg *Config) (*mux.Router, *PluginConfig) {
Expand Down Expand Up @@ -210,17 +258,32 @@ func setupProxyRoutes(cfg *Config, k8sclient *dynamic.DynamicClient, kind proxy.
return router
}

func filesHandler(root http.FileSystem) http.Handler {
fileServer := http.FileServer(root)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
filePath := r.URL.Path
type headerPreservingWriter struct {
http.ResponseWriter
wroteHeader bool
}

// disable caching for plugin entry point
if strings.HasPrefix(filePath, "/plugin-entry.js") {
func (w *headerPreservingWriter) WriteHeader(statusCode int) {
if !w.wroteHeader {
if w.Header().Get("Cache-Control") == "" {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
}
if w.Header().Get("Expires") == "" {
w.Header().Set("Expires", "0")
}
w.wroteHeader = true
}
w.ResponseWriter.WriteHeader(statusCode)
}

func filesHandler(root http.FileSystem) http.Handler {
fileServer := http.FileServer(root)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// disable caching for plugin entry point
if strings.HasPrefix(r.URL.Path, "/plugin-entry.js") {
fileServer.ServeHTTP(&headerPreservingWriter{ResponseWriter: w}, r)
return
}
fileServer.ServeHTTP(w, r)
})
}
Expand Down
Loading