diff --git a/emailsender/.gitignore b/emailsender/.gitignore new file mode 100644 index 0000000000..ae3c172604 --- /dev/null +++ b/emailsender/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/emailsender/README.md b/emailsender/README.md new file mode 100644 index 0000000000..74dc1dc54e --- /dev/null +++ b/emailsender/README.md @@ -0,0 +1,16 @@ +# ACSCS Email Service + +The email service which allows sending email notification +from ACS Central instance without configuring any additional integration. + + +## Quickstart + +```sh +# Start email sender service +CLUSTER_ID=test go run cmd/app/main.go + +... +main.go:49] Creating api server... +main.go:65] Application started. Will shut down gracefully on [interrupt terminated]. +``` diff --git a/emailsender/cmd/app/main.go b/emailsender/cmd/app/main.go new file mode 100644 index 0000000000..ef8e9e596b --- /dev/null +++ b/emailsender/cmd/app/main.go @@ -0,0 +1,74 @@ +// Package main for email sender service +package main + +import ( + "context" + "flag" + "net/http" + "os" + "os/signal" + + "github.com/gorilla/mux" + + "golang.org/x/sys/unix" + + "github.com/golang/glog" + "github.com/stackrox/acs-fleet-manager/emailsender/config" +) + +func main() { + + // This is needed to make `glog` believe that the flags have already been parsed, otherwise + // every log messages is prefixed by an error message stating the flags haven't been + // parsed. + _ = flag.CommandLine.Parse([]string{}) + + // Always log to stderr by default, required for glog. + if err := flag.Set("logtostderr", "true"); err != nil { + glog.Info("unable to set logtostderr to true.") + } + + cfg, err := config.GetConfig() + if err != nil { + glog.Errorf("Failed to load configuration: %v", err) + os.Exit(1) + } + + ctx := context.Background() + + // base router + router := mux.NewRouter() + + // example handler + router.HandleFunc("/test", func(rw http.ResponseWriter, req *http.Request) { + glog.Info("called /test endpoint") + }) + + server := http.Server{Addr: cfg.ServerAddress, Handler: router} + + go func() { + glog.Info("Creating api server...") + var err error + if cfg.EnableHTTPS { + err = server.ListenAndServeTLS(cfg.HTTPSCertFile, cfg.HTTPSKeyFile) + } else { + err = server.ListenAndServe() + } + if err != http.ErrServerClosed { + glog.Fatalf("ListenAndServer error: %v", err) + } + }() + + sigs := make(chan os.Signal, 1) + notifySignals := []os.Signal{os.Interrupt, unix.SIGTERM} + signal.Notify(sigs, notifySignals...) + + glog.Info("Application started. Will shut down gracefully on interrupt terminated OS signals") + sig := <-sigs + if err := server.Shutdown(ctx); err != nil { + glog.Errorf("API Shutdown error: %v", err) + } + + glog.Infof("Caught %s signal", sig) + glog.Info("Email sender application has been stopped") +} diff --git a/emailsender/config/config.go b/emailsender/config/config.go new file mode 100644 index 0000000000..610ef173df --- /dev/null +++ b/emailsender/config/config.go @@ -0,0 +1,43 @@ +// Package config for email sender service +package config + +import ( + "github.com/caarlos0/env/v6" + + "github.com/pkg/errors" + "github.com/stackrox/rox/pkg/errorhelpers" +) + +// Config contains this application's runtime configuration. +type Config struct { + ClusterID string `env:"CLUSTER_ID"` + ServerAddress string `env:"SERVER_ADDRESS" envDefault:":8080"` + EnableHTTPS bool `env:"ENABLE_HTTPS" envDefault:"false"` + HTTPSCertFile string `env:"HTTPS_CERT_FILE" envDefault:""` + HTTPSKeyFile string `env:"HTTPS_KEY_FILE" envDefault:""` +} + +// GetConfig retrieves the current runtime configuration from the environment and returns it. +func GetConfig() (*Config, error) { + c := Config{} + var configErrors errorhelpers.ErrorList + + if err := env.Parse(&c); err != nil { + return nil, errors.Wrap(err, "unable to parse runtime configuration from environment") + } + + if c.ClusterID == "" { + configErrors.AddError(errors.New("CLUSTER_ID environment variable is not set")) + } + + if c.EnableHTTPS { + if c.HTTPSCertFile == "" || c.HTTPSKeyFile == "" { + configErrors.AddError(errors.New("ENABLE_HTTPS is true but required variables HTTPS_CERT_FILE or HTTPS_KEY_FILE are empty")) + } + } + + if cfgErr := configErrors.ToError(); cfgErr != nil { + return nil, errors.Wrap(cfgErr, "invalid configuration settings") + } + return &c, nil +} diff --git a/emailsender/config/config_test.go b/emailsender/config/config_test.go new file mode 100644 index 0000000000..6122d3bfa8 --- /dev/null +++ b/emailsender/config/config_test.go @@ -0,0 +1,64 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetConfigSuccess(t *testing.T) { + t.Setenv("CLUSTER_ID", "test-1") + t.Setenv("SERVER_ADDRESS", ":8888") + t.Setenv("ENABLE_HTTPS", "true") + t.Setenv("HTTPS_CERT_FILE", "/some/tls.crt") + t.Setenv("HTTPS_KEY_FILE", "/some/tls.key") + + cfg, err := GetConfig() + + require.NoError(t, err) + assert.Equal(t, cfg.ClusterID, "test-1") + assert.Equal(t, cfg.ServerAddress, ":8888") + assert.Equal(t, cfg.EnableHTTPS, true) + assert.Equal(t, cfg.HTTPSCertFile, "/some/tls.crt") + assert.Equal(t, cfg.HTTPSKeyFile, "/some/tls.key") +} + +func TestGetConfigFailureMissingClusterID(t *testing.T) { + cfg, err := GetConfig() + + assert.Error(t, err) + assert.Nil(t, cfg) +} + +func TestGetConfigFailureEnabledHTTPSMissingCert(t *testing.T) { + t.Setenv("CLUSTER_ID", "test-1") + t.Setenv("ENABLE_HTTPS", "true") + t.Setenv("HTTPS_KEY_FILE", "/some/tls.key") + + cfg, err := GetConfig() + + assert.Error(t, err) + assert.Nil(t, cfg) +} + +func TestGetConfigFailureEnabledHTTPSMissingKey(t *testing.T) { + t.Setenv("CLUSTER_ID", "test-1") + t.Setenv("ENABLE_HTTPS", "true") + t.Setenv("HTTPS_CERT_FILE", "/some/tls.crt") + + cfg, err := GetConfig() + + assert.Error(t, err) + assert.Nil(t, cfg) +} + +func TestGetConfigFailureEnabledHTTPSOnly(t *testing.T) { + t.Setenv("CLUSTER_ID", "test-1") + t.Setenv("ENABLE_HTTPS", "true") + + cfg, err := GetConfig() + + assert.Error(t, err) + assert.Nil(t, cfg) +}