Skip to content

Commit

Permalink
Feat/https for everyone (#145)
Browse files Browse the repository at this point in the history
* feat: configure tls for sparrow api

Signed-off-by: Niklas Treml <[email protected]>

* feat: allow choosing scheme for targetmanager registration

Signed-off-by: Niklas Treml <[email protected]>

* docs: document scheme parameter

Signed-off-by: Niklas Treml <[email protected]>

* chore: remove dead code

Signed-off-by: Niklas Treml <[email protected]>

* fix: unit tests should now accomodate targetmanager scheme

Signed-off-by: Niklas Treml <[email protected]>

* chore: remove irrelevant comment

Signed-off-by: Niklas Treml <[email protected]>

* fix: data race in unit tests

Signed-off-by: Niklas Treml <[email protected]>

---------

Signed-off-by: Niklas Treml <[email protected]>
  • Loading branch information
niklastreml authored Jun 6, 2024
1 parent 6d2a38e commit c19585a
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 25 deletions.
38 changes: 27 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,22 @@ loader:
api:
# Which address to expose Sparrow's REST API on
address: :8080
# Configures tls for the http server
# including prometheus metrics etc
tls:
# whether to enable tls, default is false
enabled: true
# path to your x509 certificate
certPath: mycert.pem
# path to your certificate key
keyPath: mykey.key
# Configures the target manager
# Omitting this section will disable the target manager
targetManager:
# whether to enable the target manager. defaults to false
enabled: true
# Defines which target manager to use.
type: gitlab
# The interval for the target reconciliation process
Expand All @@ -244,6 +256,8 @@ targetManager:
# before it is removed from the global target list
# A duration of 0 means no removal
unhealthyThreshold: 360m
# Scheme defines with which scheme sparrow should register itself
scheme: http
# Configuration options for the GitLab target manager
gitlab:
# The URL of your GitLab host
Expand Down Expand Up @@ -304,16 +318,18 @@ the `TargetManager` interface. This feature is optional; if the startup configur
the `targetManager`, it will not be used. When configured, it offers various settings, detailed below, which can be set
in the startup YAML configuration file as shown in the [example configuration](#example-startup-configuration).

| Type | Description |
| ------------------------------------ | -------------------------------------------------------------------------------------------- |
| `targetManager.type` | Type of the target manager. Options: `gitlab` |
| `targetManager.checkInterval` | Interval for checking new targets. |
| `targetManager.unhealthyThreshold` | Threshold for marking a target as unhealthy. 0 means no cleanup. |
| `targetManager.registrationInterval` | Interval for registering the current sparrow at the target backend. 0 means no registration. |
| `targetManager.updateInterval` | Interval for updating the registration of the current sparrow. 0 means no update. |
| `targetManager.gitlab.baseUrl` | Base URL of the GitLab instance. |
| `targetManager.gitlab.token` | Token for authenticating with the GitLab instance. |
| `targetManager.gitlab.projectId` | Project ID for the GitLab project used as a remote state backend. |
| Type | Description |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `targetManager.enabled` | Whether to enable the target manager. Defaults to false |
| `targetManager.type` | Type of the target manager. Options: `gitlab` |
| `targetManager.checkInterval` | Interval for checking new targets. |
| `targetManager.unhealthyThreshold` | Threshold for marking a target as unhealthy. 0 means no cleanup. |
| `targetManager.registrationInterval` | Interval for registering the current sparrow at the target backend. 0 means no registration. |
| `targetManager.updateInterval` | Interval for updating the registration of the current sparrow. 0 means no update. |
| `targetManager.gitlab.baseUrl` | Base URL of the GitLab instance. |
| `targetManager.gitlab.token` | Token for authenticating with the GitLab instance. |
| `targetManager.gitlab.projectId` | Project ID for the GitLab project used as a remote state backend. |
| `targetManager.scheme` | Should the target register itself as http or https. Can be `http` or `https`. This needs to be set to `https`, when `api.tls.enabled` == `true` |

Currently, only one target manager exists: the Gitlab target manager. It uses a gitlab project as the remote state
backend. The various `sparrow` instances can register themselves as targets in the project.
Expand All @@ -323,7 +339,7 @@ which is named after the DNS name of the `sparrow`. The state file contains the

```json
{
"url": "https://<SPARROW_DNS_NAME>",
"url": "<SCHEME>://<SPARROW_DNS_NAME>",
"lastSeen": "2021-09-30T12:00:00Z"
}
```
Expand Down
44 changes: 38 additions & 6 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,51 @@ type API interface {
}

type api struct {
server *http.Server
router chi.Router
server *http.Server
router chi.Router
tlsConfig TLSConfig
}

// Config is the configuration for the data API
type Config struct {
ListeningAddress string `yaml:"address" mapstructure:"address"`
ListeningAddress string `yaml:"address" mapstructure:"address"`
Tls TLSConfig `yaml:"tls" mapstructure:"tls"`
}

type TLSConfig struct {
Enabled bool `yaml:"enabled" mapstructure:"enabled"`
CertPath string `yaml:"certPath" mapstructure:"certPath"`
KeyPath string `yaml:"keyPath" mapstructure:"keyPath"`
}

const (
readHeaderTimeout = 5 * time.Second
shutdownTimeout = 30 * time.Second
)

func (a *Config) Validate() error {
if a.ListeningAddress == "" {
return fmt.Errorf("listening address cannot be empty")
}
if a.Tls.Enabled {
if a.Tls.CertPath == "" {
return fmt.Errorf("tls cert path cannot be empty")
}
if a.Tls.KeyPath == "" {
return fmt.Errorf("tls key path cannot be empty")
}
}
return nil
}

// New creates a new api
func New(cfg Config) API {
r := chi.NewRouter()

return &api{
server: &http.Server{Addr: cfg.ListeningAddress, Handler: r, ReadHeaderTimeout: readHeaderTimeout},
router: r,
server: &http.Server{Addr: cfg.ListeningAddress, Handler: r, ReadHeaderTimeout: readHeaderTimeout},
router: r,
tlsConfig: cfg.Tls,
}
}

Expand All @@ -74,8 +99,15 @@ func (a *api) Run(ctx context.Context) error {
go func(cErr chan error) {
defer close(cErr)
log.Info("Serving Api", "addr", a.server.Addr)
if a.tlsConfig.Enabled {
if err := a.server.ListenAndServeTLS(a.tlsConfig.CertPath, a.tlsConfig.KeyPath); err != nil {
log.Error("Failed to serve api", "error", err, "scheme", "https")
cErr <- err
}
return
}
if err := a.server.ListenAndServe(); err != nil {
log.Error("Failed to serve api", "error", err)
log.Error("Failed to serve api", "error", err, "scheme", "http")
cErr <- err
}
}(cErr)
Expand Down
25 changes: 25 additions & 0 deletions pkg/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,28 @@ func TestAPI_OkHandler(t *testing.T) {
rr.Body.String(), expected)
}
}

func TestConfig_Validate(t *testing.T) {
cases := []struct {
name string
config Config
wantErr bool
}{
{"Empty address", Config{}, true},
{"Empty certpath", Config{Tls: TLSConfig{Enabled: true}}, true},
{"Empty keypath", Config{Tls: TLSConfig{Enabled: true}}, true},

{"Valid config", Config{ListeningAddress: ":8080"}, false},
{"Valid tls config", Config{ListeningAddress: ":8080", Tls: TLSConfig{Enabled: true, CertPath: "./mycert.pem", KeyPath: "mykey.key"}}, false},
{"Valid tls config without tls", Config{ListeningAddress: ":8080", Tls: TLSConfig{Enabled: false}}, false},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := c.config.Validate()
if (err != nil) != c.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, c.wantErr)
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ type FileLoaderConfig struct {

// HasTargetManager returns true if the config has a target manager
func (c *Config) HasTargetManager() bool {
return c.TargetManager != targets.TargetManagerConfig{}
return c.TargetManager.Enabled
}
8 changes: 6 additions & 2 deletions pkg/config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"regexp"

"github.com/caas-team/sparrow/internal/logger"
"github.com/caas-team/sparrow/pkg/sparrow/targets"
)

// Validate validates the startup config
Expand All @@ -42,13 +41,18 @@ func (c *Config) Validate(ctx context.Context) (err error) {
err = errors.Join(err, vErr)
}

if c.TargetManager != (targets.TargetManagerConfig{}) {
if c.HasTargetManager() {
if vErr := c.TargetManager.Validate(ctx); vErr != nil {
log.Error("The target manager configuration is invalid")
err = errors.Join(err, vErr)
}
}

if vErr := c.Api.Validate(); vErr != nil {
log.Error("The api configuration is invalid")
err = errors.Join(err, vErr)
}

if err != nil {
return fmt.Errorf("validation of configuration failed: %w", err)
}
Expand Down
33 changes: 33 additions & 0 deletions pkg/config/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"time"

"github.com/caas-team/sparrow/internal/helper"
"github.com/caas-team/sparrow/pkg/api"
)

func TestConfig_Validate(t *testing.T) {
Expand All @@ -38,6 +39,9 @@ func TestConfig_Validate(t *testing.T) {
name: "config ok",
config: Config{
SparrowName: "sparrow.com",
Api: api.Config{
ListeningAddress: ":8080",
},
Loader: LoaderConfig{
Type: "http",
Http: HttpLoaderConfig{
Expand All @@ -57,6 +61,9 @@ func TestConfig_Validate(t *testing.T) {
{
name: "loader - url missing",
config: Config{
Api: api.Config{
ListeningAddress: ":8080",
},
SparrowName: "sparrow.com",
Loader: LoaderConfig{
Type: "http",
Expand All @@ -77,6 +84,9 @@ func TestConfig_Validate(t *testing.T) {
{
name: "loader - url malformed",
config: Config{
Api: api.Config{
ListeningAddress: ":8080",
},
SparrowName: "sparrow.com",
Loader: LoaderConfig{
Type: "http",
Expand All @@ -96,6 +106,9 @@ func TestConfig_Validate(t *testing.T) {
{
name: "loader - retry count to high",
config: Config{
Api: api.Config{
ListeningAddress: ":8080",
},
SparrowName: "sparrow.com",
Loader: LoaderConfig{
Type: "http",
Expand All @@ -115,6 +128,26 @@ func TestConfig_Validate(t *testing.T) {
{
name: "loader - file path malformed",
config: Config{
Api: api.Config{
ListeningAddress: ":8080",
},
SparrowName: "sparrow.com",
Loader: LoaderConfig{
Type: "file",
File: FileLoaderConfig{
Path: "",
},
Interval: time.Second,
},
},
wantErr: true,
},
{
name: "targetManager - Wrong Scheme",
config: Config{
Api: api.Config{
ListeningAddress: ":8080",
},
SparrowName: "sparrow.com",
Loader: LoaderConfig{
Type: "file",
Expand Down
3 changes: 2 additions & 1 deletion pkg/sparrow/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ func TestSparrow_Run_FullComponentStart(t *testing.T) {
Interval: time.Second * 1,
},
TargetManager: targets.TargetManagerConfig{
Type: "gitlab",
Enabled: true,
Type: "gitlab",
General: targets.General{
CheckInterval: time.Second * 1,
RegistrationInterval: time.Second * 1,
Expand Down
2 changes: 2 additions & 0 deletions pkg/sparrow/targets/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ var (
ErrInvalidUpdateInterval = errors.New("invalid update interval")
// ErrInvalidInteractorType is returned when the interactor type isn't recognized
ErrInvalidInteractorType = errors.New("invalid interactor type")
// ErrInvalidScheme is returned when the scheme is not http or https
ErrInvalidScheme = errors.New("scheme must be 'http' of 'https'")
)
4 changes: 2 additions & 2 deletions pkg/sparrow/targets/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (t *manager) update(ctx context.Context) error {
AuthorEmail: fmt.Sprintf("%s@sparrow", t.name),
AuthorName: t.name,
CommitMessage: "Updated registration",
Content: checks.GlobalTarget{Url: fmt.Sprintf("https://%s", t.name), LastSeen: time.Now().UTC()},
Content: checks.GlobalTarget{Url: fmt.Sprintf("%s://%s", t.cfg.Scheme, t.name), LastSeen: time.Now().UTC()},
}
f.SetFileName(fmt.Sprintf("%s.json", t.name))

Expand Down Expand Up @@ -230,7 +230,7 @@ func (t *manager) refreshTargets(ctx context.Context) error {

// filter unhealthy targets - this may be removed in the future
for _, target := range targets {
if !t.registered && target.Url == fmt.Sprintf("https://%s", t.name) {
if !t.registered && target.Url == fmt.Sprintf("%s://%s", t.cfg.Scheme, t.name) {
log.Debug("Found self as global target", "lastSeenMin", time.Since(target.LastSeen).Minutes())
t.registered = true
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/sparrow/targets/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func Test_gitlabTargetManager_refreshTargets(t *testing.T) {
targets: nil,
interactor: remote,
name: "test",
cfg: General{UnhealthyThreshold: time.Hour},
cfg: General{UnhealthyThreshold: time.Hour, Scheme: "https"},
}
if err := gtm.refreshTargets(context.Background()); (err != nil) != (tt.wantErr != nil) {
t.Fatalf("refreshTargets() error = %v, wantErr %v", err, tt.wantErr)
Expand Down Expand Up @@ -192,7 +192,7 @@ func Test_gitlabTargetManager_refreshTargets_No_Threshold(t *testing.T) {
targets: nil,
interactor: remote,
name: "test",
cfg: General{UnhealthyThreshold: 0},
cfg: General{UnhealthyThreshold: 0, Scheme: "https"},
}
if err := gtm.refreshTargets(context.Background()); (err != nil) != (tt.wantErr != nil) {
t.Fatalf("refreshTargets() error = %v, wantErr %v", err, tt.wantErr)
Expand Down
9 changes: 9 additions & 0 deletions pkg/sparrow/targets/targetmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ type General struct {
// before it is removed from the global target list.
// A duration of 0 means no removal.
UnhealthyThreshold time.Duration `yaml:"unhealthyThreshold" mapstructure:"unhealthyThreshold"`
// Scheme is the scheme used for the remote target manager
// Can either be http or https
Scheme string `yaml:"scheme" mapstructure:"scheme"`
}

// TargetManagerConfig is the configuration for the target manager
type TargetManagerConfig struct {
Enabled bool `yaml:"enabled" mapstructure:"enabled"`
// Type defines which target manager to use
Type interactor.Type `yaml:"type" mapstructure:"type"`
// General is the general configuration of the target manager
Expand Down Expand Up @@ -85,6 +89,11 @@ func (c *TargetManagerConfig) Validate(ctx context.Context) error {
return ErrInvalidUpdateInterval
}

if c.Scheme != "http" && c.Scheme != "https" {
log.Error("The scheme should be either of: 'http', 'https'", "scheme", c.Scheme)
return ErrInvalidScheme
}

switch c.Type {
case interactor.Gitlab:
return nil
Expand Down
Loading

0 comments on commit c19585a

Please sign in to comment.