Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically reload the server after configuration changes #22

Merged
merged 18 commits into from
Sep 17, 2023
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
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