diff --git a/app/discovery/discovery.go b/app/discovery/discovery.go index 22423e0..3c3709d 100644 --- a/app/discovery/discovery.go +++ b/app/discovery/discovery.go @@ -53,7 +53,7 @@ func NewService(providers []Provider) *Service { } // Run runs blocking loop getting events from all providers -// and updating mappers on each event +// and updating all mappers on each event func (s *Service) Run(ctx context.Context) error { var evChs []<-chan struct{} @@ -79,7 +79,7 @@ func (s *Service) Run(ctx context.Context) error { } } -// Match url to all providers mappers +// Match url to all mappers func (s *Service) Match(srv, src string) (string, bool) { s.lock.RLock() diff --git a/app/discovery/provider/docker.go b/app/discovery/provider/docker.go index fc5e95e..89ec11d 100644 --- a/app/discovery/provider/docker.go +++ b/app/discovery/provider/docker.go @@ -16,11 +16,12 @@ import ( //go:generate moq -out docker_client_mock.go -skip-ensure -fmt goimports . DockerClient -// Docker provide watch compatible changes from containers -// and maps by default from ^/api/%s/(.*) to http://%s:%d/$1, i.e. http://example.com/api/my_container/something -// will be mapped to http://172.17.42.1:8080/something. Ip will be the internal ip of the container and port - exposed the one +// Docker provider watches compatible for stop/start changes from containers and maps by +// default from ^/api/%s/(.*) to http://%s:%d/$1, i.e. http://example.com/api/my_container/something +// will be mapped to http://172.17.42.1:8080/something. Ip will be the internal ip of the container and port exposed +// in the Dockerfile. // Alternatively labels can alter this. reproxy.route sets source route, and reproxy.dest sets the destination. -// Optional reproxy.server enforces match by server name (hostname). +// Optional reproxy.server enforces match by server name (hostname) and reproxy.ping sets the health check url type Docker struct { DockerClient DockerClient Excludes []string diff --git a/app/discovery/provider/file.go b/app/discovery/provider/file.go index 6f61061..9053734 100644 --- a/app/discovery/provider/file.go +++ b/app/discovery/provider/file.go @@ -14,8 +14,7 @@ import ( "github.com/umputun/reproxy/app/discovery" ) -// File implements file-based provider -// Each line contains src:dst pairs, i.e. ^/api/svc1/(.*) http://127.0.0:8080/blah/$1 +// File implements file-based provider, defined with yaml file type File struct { FileName string CheckInterval time.Duration diff --git a/app/discovery/provider/static.go b/app/discovery/provider/static.go index a3ef564..c8c29f5 100644 --- a/app/discovery/provider/static.go +++ b/app/discovery/provider/static.go @@ -12,7 +12,7 @@ import ( // Static provider, rules are server,from,to type Static struct { - Rules []string // each rule is 5 elements comma separated. server,source url,destination,ping + Rules []string // each rule is 4 elements comma separated - server,source_url,destination,ping } // Events returns channel updating once diff --git a/app/proxy/health_test.go b/app/proxy/health_test.go new file mode 100644 index 0000000..873e5a9 --- /dev/null +++ b/app/proxy/health_test.go @@ -0,0 +1,72 @@ +package proxy + +import ( + "context" + "encoding/json" + "fmt" + "math/rand" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/umputun/reproxy/app/discovery" + "github.com/umputun/reproxy/app/discovery/provider" +) + +func TestHttp_healthHandler(t *testing.T) { + port := rand.Intn(10000) + 40000 + h := Http{TimeOut: 200 * time.Millisecond, Address: fmt.Sprintf("127.0.0.1:%d", port)} + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + ds := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("req: %v", r) + w.Header().Add("h1", "v1") + fmt.Fprintf(w, "response %s", r.URL.String()) + })) + + ps := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("req: %v", r) + if r.URL.Path == "/123/ping" { + return + } + w.WriteHeader(http.StatusBadRequest) + })) + + svc := discovery.NewService([]discovery.Provider{ + &provider.Static{Rules: []string{ + "localhost,^/api/(.*)," + ds.URL + "/123/$1," + ps.URL + "/123/ping", + "127.0.0.1,^/api/(.*)," + ds.URL + "/567/$1," + ps.URL + "/567/ping", + }, + }}) + + go func() { + _ = svc.Run(context.Background()) + }() + + h.Matcher = svc + go func() { + _ = h.Run(ctx) + }() + time.Sleep(10 * time.Millisecond) + + client := http.Client{} + req, err := http.NewRequest("GET", "http://127.0.0.1:"+strconv.Itoa(port)+"/health", nil) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusExpectationFailed, resp.StatusCode) + + res := map[string]interface{}{} + err = json.NewDecoder(resp.Body).Decode(&res) + require.NoError(t, err) + assert.Equal(t, "failed", res["status"]) + assert.Equal(t, 1., res["passed"]) + assert.Equal(t, 1., res["failed"]) + assert.Contains(t, res["errors"], "400 Bad Request") +} diff --git a/app/proxy/proxy_test.go b/app/proxy/proxy_test.go index a1a88f3..132128a 100644 --- a/app/proxy/proxy_test.go +++ b/app/proxy/proxy_test.go @@ -2,7 +2,6 @@ package proxy import ( "context" - "encoding/json" "fmt" "io" "math/rand" @@ -90,6 +89,72 @@ func TestHttp_Do(t *testing.T) { } } +func TestHttp_DoWithAssets(t *testing.T) { + port := rand.Intn(10000) + 40000 + h := Http{TimeOut: 200 * time.Millisecond, Address: fmt.Sprintf("127.0.0.1:%d", port), + AccessLog: io.Discard, AssetsWebRoot: "/static", AssetsLocation: "testdata", DisableSignature: true} + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + ds := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("req: %v", r) + w.Header().Add("h1", "v1") + require.Equal(t, "127.0.0.1", r.Header.Get("X-Real-IP")) + fmt.Fprintf(w, "response %s", r.URL.String()) + })) + + svc := discovery.NewService([]discovery.Provider{ + &provider.Static{Rules: []string{ + "localhost,^/api/(.*)," + ds.URL + "/123/$1,", + "127.0.0.1,^/api/(.*)," + ds.URL + "/567/$1,", + }, + }}) + + go func() { + _ = svc.Run(context.Background()) + }() + + h.Matcher = svc + go func() { + _ = h.Run(ctx) + }() + time.Sleep(10 * time.Millisecond) + + client := http.Client{} + + { + req, err := http.NewRequest("GET", "http://127.0.0.1:"+strconv.Itoa(port)+"/api/something", nil) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + t.Logf("%+v", resp.Header) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "response /567/something", string(body)) + assert.Equal(t, "", resp.Header.Get("App-Name")) + assert.Equal(t, "v1", resp.Header.Get("h1")) + } + + { + resp, err := client.Get("http://localhost:" + strconv.Itoa(port) + "/static/1.html") + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + t.Logf("%+v", resp.Header) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "test html", string(body)) + assert.Equal(t, "", resp.Header.Get("App-Name")) + assert.Equal(t, "", resp.Header.Get("h1")) + + } + +} + func TestHttp_toHttp(t *testing.T) { tbl := []struct { @@ -111,57 +176,3 @@ func TestHttp_toHttp(t *testing.T) { } } - -func TestHttp_healthHandler(t *testing.T) { - port := rand.Intn(10000) + 40000 - h := Http{TimeOut: 200 * time.Millisecond, Address: fmt.Sprintf("127.0.0.1:%d", port)} - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - - ds := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Logf("req: %v", r) - w.Header().Add("h1", "v1") - fmt.Fprintf(w, "response %s", r.URL.String()) - })) - - ps := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Logf("req: %v", r) - if r.URL.Path == "/123/ping" { - return - } - w.WriteHeader(http.StatusBadRequest) - })) - - svc := discovery.NewService([]discovery.Provider{ - &provider.Static{Rules: []string{ - "localhost,^/api/(.*)," + ds.URL + "/123/$1," + ps.URL + "/123/ping", - "127.0.0.1,^/api/(.*)," + ds.URL + "/567/$1," + ps.URL + "/567/ping", - }, - }}) - - go func() { - _ = svc.Run(context.Background()) - }() - - h.Matcher = svc - go func() { - _ = h.Run(ctx) - }() - time.Sleep(10 * time.Millisecond) - - client := http.Client{} - req, err := http.NewRequest("GET", "http://127.0.0.1:"+strconv.Itoa(port)+"/health", nil) - require.NoError(t, err) - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - assert.Equal(t, http.StatusExpectationFailed, resp.StatusCode) - - res := map[string]interface{}{} - err = json.NewDecoder(resp.Body).Decode(&res) - require.NoError(t, err) - assert.Equal(t, "failed", res["status"]) - assert.Equal(t, 1., res["passed"]) - assert.Equal(t, 1., res["failed"]) - assert.Contains(t, res["errors"], "400 Bad Request") -} diff --git a/app/proxy/testdata/1.html b/app/proxy/testdata/1.html new file mode 100644 index 0000000..3666d24 --- /dev/null +++ b/app/proxy/testdata/1.html @@ -0,0 +1 @@ +test html \ No newline at end of file diff --git a/reproxy.service b/reproxy.service index ab2d11c..b1b0cb3 100644 --- a/reproxy.service +++ b/reproxy.service @@ -8,7 +8,7 @@ Type=simple Restart=always RestartSec=1 User=root -ExecStart=/usr/bin/reproxy -f /etc/reproxy.yml -u +ExecStart=/usr/bin/reproxy --file.enabled --file.name=/etc/reproxy.yml [Install] WantedBy=multi-user.target \ No newline at end of file