Skip to content

Commit

Permalink
Refactor make config independent (cristianoliveira#86)
Browse files Browse the repository at this point in the history
* refactor: Change config to be independent

* fix: Update config using channel signals

* refactor: Group methods by meaning
  • Loading branch information
cristianoliveira authored Nov 13, 2017
1 parent 364093c commit 653ed42
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 99 deletions.
8 changes: 3 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ func command() func() {
command.StringVar(&config.Domain, "domain", ".dev", "Set a custom domain for services")
command.Parse(os.Args[2:])

var err error
config.Services, err = proxy.LoadServices(config.ConfigFile)
err := config.LoadServices()
if err != nil {
log.Fatalf("Could not load services: %v\n", err)
return nil
Expand Down Expand Up @@ -112,11 +111,10 @@ func command() func() {
command.StringVar(&config.ConfigFile, "config", "./.ergo", "Set the services file")
command.Parse(os.Args[4:])

services, err := proxy.LoadServices(config.ConfigFile)
err := config.LoadServices()
if err != nil {
log.Printf("Could not load services: %v\n", err)
log.Fatalf("Could not load services: %v\n", err)
}
config.Services = services

return execute(commands.AddServiceCommand{Service: service}, config)

Expand Down
96 changes: 71 additions & 25 deletions proxy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,11 @@ import (
"log"
"os"
"regexp"
"sync"
"time"
)

var (
modTime time.Time
size int64
)

var configChan = make(chan []Service, 1)
const pollIntervall = 500

//Service holds the details of the service (Name and URL)
type Service struct {
Expand All @@ -25,6 +21,10 @@ type Service struct {

//Config holds the configuration for the proxy.
type Config struct {
mutex *sync.Mutex
lastChange time.Time
size int64

Port string
Domain string
URLPattern string
Expand All @@ -37,12 +37,7 @@ type Config struct {
func (c *Config) GetService(host string) *Service {
domainPattern := regexp.MustCompile(`(\w*\:\/\/)?(.+)` + c.Domain)
parts := domainPattern.FindAllString(host, -1)
//we must lock the access as the configuration can be dynamically loaded
select {
case srv := <-configChan:
c.Services = srv
default:
}

for _, s := range c.Services {
if len(parts) > 0 && s.Name+c.Domain == parts[0] {
return &s
Expand Down Expand Up @@ -77,29 +72,81 @@ func NewService(name, url string) Service {

//LoadServices loads the services from filepath, returns an error
//if the configuration could not be parsed
func LoadServices(filepath string) ([]Service, error) {
func (c *Config) LoadServices() error {
services, err := LoadServicesFromConfig(c.ConfigFile)
if err != nil {
return err
}

info, err := os.Stat(filepath)
c.getMutex().Lock()
defer c.getMutex().Unlock()
c.Services = services

if err != nil {
return nil, err
return nil
}

func (c *Config) getMutex() *sync.Mutex {
if c.mutex == nil {
c.mutex = &sync.Mutex{}
}

size = info.Size()
modTime = info.ModTime()
return c.mutex
}

file, e := os.Open(filepath)
// ListenServices updates the services for each message in a given channel
func (c *Config) ListenServices(servicesSignal <-chan []Service) {
for {
select {
case services := <-servicesSignal:
c.getMutex().Lock()
c.Services = services
c.mutex.Unlock()
}
}
}

if e != nil {
return nil, fmt.Errorf("file error: %v", e)
// WatchConfigFile listen for file changes and sends signal for updates
func (c *Config) WatchConfigFile(servicesChan chan []Service) {
ticker := time.NewTicker(pollIntervall * time.Millisecond)
quit = make(chan struct{})

for {
select {
case <-ticker.C:
info, err := os.Stat(c.ConfigFile)
if err != nil {
log.Printf("Error reading config file: %s\r\n", err.Error())
continue
}

if info.ModTime().Before(c.lastChange) || info.Size() != c.size {
services, err := LoadServicesFromConfig(c.ConfigFile)
if err != nil {
log.Printf("Error reading the modified config file: %s\r\n", err.Error())
continue
}

c.size = info.Size()
c.lastChange = info.ModTime()
servicesChan <- services
}

case <-quit:
ticker.Stop()
return
}
}
}

// LoadServicesFromConfig reads the given path and parse it into services
func LoadServicesFromConfig(filepath string) ([]Service, error) {
file, err := os.Open(filepath)
if err != nil {
return nil, fmt.Errorf("file error: %v", err)
}
defer file.Close()

log.Println("Just received ", filepath)

services := []Service{}

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
Expand Down Expand Up @@ -137,7 +184,6 @@ func AddService(filepath string, service Service) error {
// RemoveService removes a service from the filepath
func RemoveService(filepath string, service Service) error {
file, err := ioutil.ReadFile(filepath)

if err != nil {
log.Printf("File error: %v\n", err)
return err
Expand Down
9 changes: 4 additions & 5 deletions proxy/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import (

func TestWhenHasErgoFile(t *testing.T) {
config := NewConfig()
services, err := LoadServices("../.ergo")
config.ConfigFile = "../.ergo"
err := config.LoadServices()
if err != nil {
t.Fatal("could not load requied configuration file for tests")
}

config.Services = services

t.Run("It loads the services redirections", func(t *testing.T) {
expected := 6
result := len(config.Services)
Expand Down Expand Up @@ -152,8 +151,8 @@ func TestWhenHasErgoFile(t *testing.T) {
defer ioutil.WriteFile("../.ergo", fileContent, 0755)

service := NewService("service-without-url", "")
AddService("../.ergo", service)
_, err = LoadServices("../.ergo")
AddService(config.ConfigFile, service)
err = config.LoadServices()
if err == nil {
tt.Error("Expected LoadServices to fail")
}
Expand Down
47 changes: 6 additions & 41 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package proxy

import (
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"time"
)
Expand All @@ -15,8 +13,6 @@ import (
//when the configuration watcher should stop
var quit chan struct{}

const pollIntervall = 500

func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
Expand All @@ -42,43 +38,6 @@ func formatRequest(r *http.Request) string {
return strings.Join(request, "\n")
}

func pollConfigChange(config *Config) {
ticker := time.NewTicker(pollIntervall * time.Millisecond)

quit = make(chan struct{})

go func() {
for {
select {
case <-ticker.C:
info, err := os.Stat(config.ConfigFile)
if err != nil {
log.Printf("Error reading config file: %s\r\n", err.Error())
continue
}

if info.ModTime().Before(modTime) || info.Size() != size {
services, err := LoadServices(config.ConfigFile)
if err != nil {
log.Printf("Error reading the modified config file: %s\r\n", err.Error())
continue
}
//clear the data if there is any
select {
case <-configChan:
default:
}
configChan <- services
}

case <-quit:
ticker.Stop()
return
}
}
}()
}

//NewErgoProxy returns the new reverse proxy.
func NewErgoProxy(config *Config) *httputil.ReverseProxy {
director := func(req *http.Request) {
Expand Down Expand Up @@ -146,6 +105,12 @@ func list(config *Config) func(w http.ResponseWriter, r *http.Request) {
}
}

func pollConfigChange(config *Config) {
servicesSignal := make(chan []Service)
go config.ListenServices(servicesSignal)
go config.WatchConfigFile(servicesSignal)
}

//ServeProxy listens & serves the HTTP proxy.
func ServeProxy(config *Config) error {

Expand Down
42 changes: 19 additions & 23 deletions proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import (

func TestWhenHasCollectionFile(t *testing.T) {
config := NewConfig()
services, err := LoadServices("../.ergo")
config.ConfigFile = "../.ergo"
err := config.LoadServices()
if err != nil {
t.Fatal("could not load requied configuration file for tests")
}
config.Services = services

proxy := NewErgoProxy(config)

t.Run("it redirects foo.dev to localhost 3000", func(t *testing.T) {
Expand Down Expand Up @@ -184,8 +185,6 @@ func TestPollConfigChangeWithInvalidConfigFile(t *testing.T) {

pollConfigChange(&config)

stop := struct{}{}

time.Sleep(2 * time.Second)
logbuf := new(bytes.Buffer)

Expand All @@ -199,8 +198,6 @@ func TestPollConfigChangeWithInvalidConfigFile(t *testing.T) {

time.Sleep(2 * time.Second)

quit <- stop

if len(logbuf.String()) == 0 {
t.Fatalf("Expected to get a read from the log. Got none")
} else if !strings.Contains(logbuf.String(), "Error reading the modified config file") {
Expand All @@ -226,36 +223,34 @@ func TestPollConfigChangeWithValidConfigFile(t *testing.T) {

config := Config{}
config.ConfigFile = tmpfile.Name()
config.LoadServices()

pollConfigChange(&config)

stop := struct{}{}

time.Sleep(2 * time.Second)

err = ioutil.WriteFile(tmpfile.Name(), []byte("test.dev http://localhost:9900"), 0644)

configFile, err := os.OpenFile(config.ConfigFile, os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
t.Fatalf("Error while opening the config file %s", err.Error())
}

defer configFile.Close()

if _, err = configFile.WriteString("\ntest.dev http://localhost:9900"); err != nil {
t.Fatalf("Expected no error while rewriting the temporary config file. Got %s", err.Error())
}

time.Sleep(2 * time.Second)

var srv []Service

select {
case srv = <-configChan:
if len(srv) != 1 {
t.Fatalf("Expected to get 1 service Got %d", len(srv))
}
if srv[0].URL != "http://localhost:9900" || srv[0].Name != "test.dev" {
t.Fatalf("Expected to get 1 service with the URL http://localhost:9900 and the name test.dev. Got the URL: %s and the name: %s", srv[0].URL, srv[0].Name)
}
default:
quit <- stop
t.Fatalf("Expected to get the changed services. Got nothing")
if len(config.Services) != 2 {
t.Fatalf("Expected to get 2 service Got %d", len(srv))
}

if config.Services[1].URL != "http://localhost:9900" || config.Services[1].Name != "test.dev" {
t.Fatalf("Expected to get 1 service with the URL http://localhost:9900 and the name test.dev. Got the URL: %s and the name: %s", config.Services[1].URL, config.Services[1].Name)
}
quit <- stop
}

//structure to mock a http ResponseWriter
Expand Down Expand Up @@ -356,7 +351,8 @@ func TestListFunction(t *testing.T) {
config.ConfigFile = tmpfile.Name()
config.Domain = "dev"
config.Port = "2000"
config.Services, err = LoadServices(tmpfile.Name())
config.ConfigFile = tmpfile.Name()
err = config.LoadServices()
if err != nil {
t.Fatalf("Expected no error while loading services from temp file. Got %s", err.Error())
}
Expand Down

0 comments on commit 653ed42

Please sign in to comment.