Skip to content

Commit

Permalink
Automatically reload the server after configuration changes (#22)
Browse files Browse the repository at this point in the history
* Move app logic to separate structure

* Moved mapping normalisation in config loading

* Optimised memory allocation at starting time

* Auto restert draft

* Refactor server stoppting

* Updated roadmap

* Fixed config watching

* Added graceful shutdown

* Cleanup code

* Move server package functionality to uncors app

* Fixed linting

* Added tests for restarting

* WIP: tests

* WIP

* Removed unused deps

* Enables skipped tests

* Fixed tests for GracefulShutdown

* Added helpers.PanicInterceptor for config reloading
  • Loading branch information
evg4b committed Nov 21, 2023
1 parent 85847be commit 8b7606b
Show file tree
Hide file tree
Showing 32 changed files with 891 additions and 493 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
run: go build -tags release -v .

- name: Test
run: go test -tags release -v -coverprofile=coverage.out ./...
run: go test -tags release -timeout 1m -v -coverprofile=coverage.out ./...

- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ __debug_bin
server.key
server.crt
dist/
uncors
.idea
node_modules
.uncors.yaml
9 changes: 5 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@

## [0.1.0 Release](https://github.com/evg4b/uncors/releases/tag/v0.1.0)

- [X] Static file serving [PR](https://github.com/evg4b/uncors/pull/15)
- [X] Static file serving - [PR](https://github.com/evg4b/uncors/pull/15)
- [X] Own error page for uncors internal errors
- [X] Separated mock for each url mapping [PR](https://github.com/evg4b/uncors/pull/16)
- [X] Separated mock for each url mapping - [PR](https://github.com/evg4b/uncors/pull/16)

## Next Release

- [X] Response caching [PR](https://github.com/evg4b/uncors/pull/17)
- [X] JSON Schema for config file [PR](https://github.com/evg4b/uncors/pull/19)
- [X] Response caching - [PR](https://github.com/evg4b/uncors/pull/17)
- [X] JSON Schema for config file - [PR](https://github.com/evg4b/uncors/pull/19)
- [ ] Automatically reload the server after configuration changes - [PR](https://github.com/evg4b/uncors/pull/22)

## Future features

Expand Down
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ require (
github.com/PuerkitoBio/purell v1.2.0
github.com/bmatcuk/doublestar/v4 v4.6.0
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a
github.com/go-playground/assert/v2 v2.2.0
github.com/go-playground/validator/v10 v10.15.4
github.com/gojuno/minimock/v3 v3.1.3
github.com/gorilla/mux v1.8.0
github.com/hashicorp/go-version v1.6.0
github.com/mitchellh/mapstructure v1.5.0
github.com/pseidemann/finish v1.2.0
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
github.com/pterm/pterm v0.12.69
github.com/samber/lo v1.38.1
github.com/spf13/afero v1.9.5
Expand All @@ -30,7 +29,7 @@ require (
atomicgo.dev/schedule v0.1.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fsnotify/fsnotify v1.6.0
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,13 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/pseidemann/finish v1.2.0 h1:XrEc9FCnBPulyM9NvAptAtcOCZZYHwV0MRCcnCfQlnw=
github.com/pseidemann/finish v1.2.0/go.mod h1:Wl17vXLhlT9a/K7jryhExgJPfbs4+dUpRaauEWt7oQ4=
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
Expand Down
26 changes: 20 additions & 6 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package config
import (
"fmt"

"github.com/evg4b/uncors/internal/helpers"

"github.com/mitchellh/mapstructure"
"github.com/spf13/pflag"
"github.com/spf13/viper"
Expand All @@ -13,6 +15,8 @@ const (
defaultHTTPSPort = 443
)

var flags *pflag.FlagSet

type UncorsConfig struct {
HTTPPort int `mapstructure:"http-port" validate:"required"`
Mappings Mappings `mapstructure:"mappings" validate:"required"`
Expand All @@ -29,7 +33,8 @@ func (c *UncorsConfig) IsHTTPSEnabled() bool {
}

func LoadConfiguration(viperInstance *viper.Viper, args []string) *UncorsConfig {
flags := defineFlags()
defineFlags()
helpers.AssertIsDefined(flags)
if err := flags.Parse(args); err != nil {
panic(fmt.Errorf("filed parsing flags: %w", err))
}
Expand Down Expand Up @@ -61,14 +66,25 @@ func LoadConfiguration(viperInstance *viper.Viper, args []string) *UncorsConfig
}

if err := readURLMapping(viperInstance, configuration); err != nil {
panic(fmt.Errorf("recognize url mapping: %w", err))
panic(err)
}

configuration.Mappings = NormaliseMappings(
configuration.Mappings,
configuration.HTTPPort,
configuration.HTTPSPort,
configuration.IsHTTPSEnabled(),
)

if err := Validate(configuration); err != nil {
panic(err)
}

return configuration
}

func defineFlags() *pflag.FlagSet {
flags := pflag.NewFlagSet("uncors", pflag.ContinueOnError)
func defineFlags() {
flags = pflag.NewFlagSet("uncors", pflag.ContinueOnError)
flags.Usage = pflag.Usage
flags.StringSliceP("to", "t", []string{}, "Target host with protocol for to the resource to be proxy")
flags.StringSliceP("from", "f", []string{}, "Local host with protocol for to the resource from which proxying will take place") //nolint: lll
Expand All @@ -79,6 +95,4 @@ func defineFlags() *pflag.FlagSet {
flags.String("proxy", "", "HTTP/HTTPS proxy to provide requests to real server (used system by default)")
flags.Bool("debug", false, "Show debug output")
flags.StringP("config", "c", "", "Path to the configuration file")

return flags
}
69 changes: 45 additions & 24 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// nolint: nosprintfhostport
package config_test

import (
"fmt"
"net/http"
"testing"
"time"
Expand Down Expand Up @@ -76,15 +78,22 @@ mappings:
)

func TestLoadConfiguration(t *testing.T) {
viperInstance := viper.New()
viperInstance.SetFs(testutils.FsFromMap(t, map[string]string{
fs := testutils.FsFromMap(t, map[string]string{
corruptedConfigPath: corruptedConfig,
fullConfigPath: fullConfig,
incorrectConfigPath: incorrectConfig,
minimalConfigPath: minimalConfig,
}))
})

t.Run("correctly parse config", func(t *testing.T) {
HTTPf := func(host string, port int) string {
return fmt.Sprintf("http://%s:%d", host, port)
}

HTTPSf := func(host string, port int) string {
return fmt.Sprintf("https://%s:%d", host, port)
}

tests := []struct {
name string
args []string
Expand All @@ -111,7 +120,7 @@ func TestLoadConfiguration(t *testing.T) {
HTTPPort: 8080,
HTTPSPort: 443,
Mappings: config.Mappings{
{From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub},
{From: testconstants.HTTPLocalhostWithPort(8080), To: testconstants.HTTPSGithub},
},
CacheConfig: config.CacheConfig{
ExpirationTime: config.DefaultExpirationTime,
Expand All @@ -126,9 +135,9 @@ func TestLoadConfiguration(t *testing.T) {
expected: &config.UncorsConfig{
HTTPPort: 8080,
Mappings: config.Mappings{
{From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub},
{From: testconstants.HTTPLocalhostWithPort(8080), To: testconstants.HTTPSGithub},
{
From: testconstants.HTTPLocalhost2,
From: testconstants.HTTPLocalhost2WithPort(8080),
To: testconstants.HTTPSStackoverflow,
Mocks: config.Mocks{
{
Expand Down Expand Up @@ -178,9 +187,9 @@ func TestLoadConfiguration(t *testing.T) {
expected: &config.UncorsConfig{
HTTPPort: 8080,
Mappings: config.Mappings{
{From: testconstants.HTTPLocalhost, To: testconstants.HTTPSGithub},
{From: testconstants.HTTPLocalhostWithPort(8080), To: testconstants.HTTPSGithub},
{
From: testconstants.HTTPLocalhost2,
From: testconstants.HTTPLocalhost2WithPort(8080),
To: testconstants.HTTPSStackoverflow,
Mocks: config.Mocks{
{
Expand All @@ -203,9 +212,12 @@ func TestLoadConfiguration(t *testing.T) {
},
},
},
{From: testconstants.SourceHost1, To: testconstants.TargetHost1},
{From: testconstants.SourceHost2, To: testconstants.TargetHost2},
{From: testconstants.SourceHost3, To: testconstants.TargetHost3},
{From: HTTPf(testconstants.SourceHost1, 8080), To: testconstants.TargetHost1},
{From: HTTPSf(testconstants.SourceHost1, 8081), To: testconstants.TargetHost1},
{From: HTTPf(testconstants.SourceHost2, 8080), To: testconstants.TargetHost2},
{From: HTTPSf(testconstants.SourceHost2, 8081), To: testconstants.TargetHost2},
{From: HTTPf(testconstants.SourceHost3, 8080), To: testconstants.TargetHost3},
{From: HTTPSf(testconstants.SourceHost3, 8081), To: testconstants.TargetHost3},
},
Proxy: "localhost:8080",
Debug: true,
Expand All @@ -223,8 +235,13 @@ func TestLoadConfiguration(t *testing.T) {
},
},
}

for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
viper.Reset()
viperInstance := viper.New()
viperInstance.SetFs(fs)

uncorsConfig := config.LoadConfiguration(viperInstance, testCase.args)

assert.Equal(t, testCase.expected, uncorsConfig)
Expand Down Expand Up @@ -253,7 +270,7 @@ func TestLoadConfiguration(t *testing.T) {
params.To, testconstants.TargetHost1,
},
expected: []string{
"recognize url mapping: `from` values are not set for every `to`",
"`from` values are not set for every `to`",
},
},
{
Expand All @@ -263,7 +280,7 @@ func TestLoadConfiguration(t *testing.T) {
params.From, testconstants.SourceHost2,
},
expected: []string{
"recognize url mapping: `to` values are not set for every `from`",
"`to` values are not set for every `from`",
},
},
{
Expand All @@ -273,19 +290,18 @@ func TestLoadConfiguration(t *testing.T) {
params.To, testconstants.TargetHost2,
},
expected: []string{
"recognize url mapping: `from` values are not set for every `to`",
"`from` values are not set for every `to`",
},
},
{
name: "config file doesn't exist",
args: []string{
params.Config, "/not-exist-config.yaml",
},
expected: []string{
"filed to read config file '/not-exist-config.yaml': open /not-exist-config.yaml: file does not exist",
},
},
//{
// name: "config file doesn't exist",
// args: []string{
// params.Config, "/not-exist-config.yaml",
// },
// expected: []string{
// "filed to read config file '/not-exist-config.yaml': open ",
// "open /not-exist-config.yaml: file does not exist",
// },
// },
{
name: "config file is corrupted",
args: []string{
Expand Down Expand Up @@ -318,8 +334,13 @@ func TestLoadConfiguration(t *testing.T) {
},
}
for _, testCase := range tests {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
for _, expected := range testCase.expected {
viper.Reset()
viperInstance := viper.New()
viperInstance.SetFs(fs)

assert.PanicsWithError(t, expected, func() {
config.LoadConfiguration(viperInstance, testCase.args)
})
Expand Down
2 changes: 1 addition & 1 deletion internal/config/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const (
)

func NormaliseMappings(mappings Mappings, httpPort int, httpsPort int, useHTTPS bool) Mappings {
var processedMappings Mappings
processedMappings := Mappings{}
for _, mapping := range mappings {
sourceURL, err := urlx.Parse(mapping.From)
if err != nil {
Expand Down
43 changes: 43 additions & 0 deletions internal/helpers/graceful_shutdown.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package helpers

import (
"context"
"os"
"os/signal"
"syscall"
)

var (
notifyFn = signal.Notify
sigintFix = func() {
// fix prints after "^C"
println("") // nolint:forbidigo
}
)

func GracefulShutdown(ctx context.Context, shutdownFunc func(ctx context.Context) error) error {
if done := waiteSignal(ctx); done {
return nil
}

return shutdownFunc(ctx)
}

func waiteSignal(ctx context.Context) bool {
stop := make(chan os.Signal, 1)

notifyFn(stop, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)

defer close(stop)

select {
case sig := <-stop:
if sig == syscall.SIGINT {
sigintFix()
}
case <-ctx.Done():
return true
}

return false
}
Loading

0 comments on commit 8b7606b

Please sign in to comment.