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

Support for reverse proxy routing with custom path. #237

Merged
merged 11 commits into from May 9, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Expand Up @@ -25,12 +25,12 @@ If you're working on the frontend and can use mocked data rather than a real bac
```
cd webapp/frontend
npm install
ng serve
ng serve --deploy-url="/web/" --base-href="/web/"
```

However, if you need to also run the backend, and use real data, you'll need to run the following command:
```
cd webapp/frontend && ng build --watch --output-path=../../dist --deploy-url="/web/" --base-href="/web/" --prod
cd webapp/frontend && ng build --watch --output-path=../../dist --prod
```

> Note: if you do not add `--prod` flag, app will display mocked data for api calls.
Expand Down
6 changes: 5 additions & 1 deletion collector/cmd/collector-metrics/collector-metrics.go
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"log"
"os"
"strings"
"time"

utils "github.com/analogj/go-util/utils"
Expand Down Expand Up @@ -113,7 +114,10 @@ OPTIONS:
}

if c.IsSet("api-endpoint") {
config.Set("api.endpoint", c.String("api-endpoint"))
//if the user is providing an api-endpoint with a basepath (eg. http://localhost:8080/scrutiny),
//we need to ensure the basepath has a trailing slash, otherwise the url.Parse() path concatenation doesnt work.
apiEndpoint := strings.TrimSuffix(c.String("api-endpoint"), "/") + "/"
config.Set("api.endpoint", apiEndpoint)
}

collectorLogger := logrus.WithFields(logrus.Fields{
Expand Down
5 changes: 3 additions & 2 deletions collector/pkg/collector/metrics.go
Expand Up @@ -48,7 +48,7 @@ func (mc *MetricsCollector) Run() error {
}

apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
apiEndpoint.Path = "/api/devices/register"
apiEndpoint, _ = apiEndpoint.Parse("api/devices/register") //this acts like filepath.Join()

deviceRespWrapper := new(models.DeviceWrapper)

Expand All @@ -73,6 +73,7 @@ func (mc *MetricsCollector) Run() error {

if !deviceRespWrapper.Success {
mc.logger.Errorln("An error occurred while retrieving filtered devices")
mc.logger.Debugln(deviceRespWrapper)
return errors.ApiServerCommunicationError("An error occurred while retrieving filtered devices")
} else {
mc.logger.Debugln(deviceRespWrapper)
Expand Down Expand Up @@ -146,7 +147,7 @@ func (mc *MetricsCollector) Publish(deviceWWN string, payload []byte) error {
mc.logger.Infof("Publishing smartctl results for %s\n", deviceWWN)

apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
apiEndpoint.Path = fmt.Sprintf("/api/device/%s/smart", strings.ToLower(deviceWWN))
apiEndpoint, _ = apiEndpoint.Parse(fmt.Sprintf("api/device/%s/smart", strings.ToLower(deviceWWN)))

resp, err := httpClient.Post(apiEndpoint.String(), "application/json", bytes.NewBuffer(payload))
if err != nil {
Expand Down
38 changes: 38 additions & 0 deletions collector/pkg/collector/metrics_test.go
@@ -0,0 +1,38 @@
package collector

import (
"github.com/stretchr/testify/require"
"net/url"
"testing"
)

func TestApiEndpointParse(t *testing.T) {
baseURL, _ := url.Parse("http://localhost:8080/")

url1, _ := baseURL.Parse("d/e")
require.Equal(t, "http://localhost:8080/d/e", url1.String())

url2, _ := baseURL.Parse("/d/e")
require.Equal(t, "http://localhost:8080/d/e", url2.String())
}

func TestApiEndpointParse_WithBasepathWithoutTrailingSlash(t *testing.T) {
baseURL, _ := url.Parse("http://localhost:8080/scrutiny")

//This testcase is unexpected and can cause issues. We need to ensure the apiEndpoint always has a trailing slash.
url1, _ := baseURL.Parse("d/e")
require.Equal(t, "http://localhost:8080/d/e", url1.String())

url2, _ := baseURL.Parse("/d/e")
require.Equal(t, "http://localhost:8080/d/e", url2.String())
}

func TestApiEndpointParse_WithBasepathWithTrailingSlash(t *testing.T) {
baseURL, _ := url.Parse("http://localhost:8080/scrutiny/")

url1, _ := baseURL.Parse("d/e")
require.Equal(t, "http://localhost:8080/scrutiny/d/e", url1.String())

url2, _ := baseURL.Parse("/d/e")
require.Equal(t, "http://localhost:8080/d/e", url2.String())
}
2 changes: 1 addition & 1 deletion docker/Dockerfile
Expand Up @@ -22,7 +22,7 @@ COPY webapp/frontend /opt/scrutiny/src
RUN npm install -g @angular/cli@9.1.4 && \
mkdir -p /scrutiny/dist && \
npm install && \
ng build --output-path=/opt/scrutiny/dist --deploy-url="/web/" --base-href="/web/" --prod
ng build --output-path=/opt/scrutiny/dist --prod


########
Expand Down
2 changes: 1 addition & 1 deletion docker/Dockerfile.web
Expand Up @@ -20,7 +20,7 @@ COPY webapp/frontend /opt/scrutiny/src
RUN npm install -g @angular/cli@9.1.4 && \
mkdir -p /opt/scrutiny/dist && \
npm install && \
ng build --output-path=/opt/scrutiny/dist --deploy-url="/web/" --base-href="/web/" --prod
ng build --output-path=/opt/scrutiny/dist --prod


########
Expand Down
8 changes: 8 additions & 0 deletions example.scrutiny.yaml
Expand Up @@ -20,10 +20,18 @@ web:
listen:
port: 8080
host: 0.0.0.0

# if you're using a reverse proxy like apache/nginx, you can override this value to serve scrutiny on a subpath.
# eg. http://example.com/scrutiny/* vs http://example.com:8080
# see docs/TROUBLESHOOTING_REVERSE_PROXY.md
# basepath: `/scrutiny`
# leave empty unless behind a path prefixed proxy
basepath: ''
database:
# can also set absolute path here
location: /opt/scrutiny/config/scrutiny.db
src:
# the location on the filesystem where scrutiny javascript + css is located
frontend:
path: /opt/scrutiny/web
influxdb:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -4,6 +4,7 @@ go 1.13

require (
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
github.com/citilinkru/libudev v1.0.0
github.com/containrrr/shoutrrr v0.4.4
github.com/fatih/color v1.10.0
github.com/gin-gonic/gin v1.6.3
Expand Down
1 change: 1 addition & 0 deletions webapp/backend/pkg/config/config.go
Expand Up @@ -30,6 +30,7 @@ func (c *configuration) Init() error {
//set defaults
c.SetDefault("web.listen.port", "8080")
c.SetDefault("web.listen.host", "0.0.0.0")
c.SetDefault("web.listen.basepath", "")
c.SetDefault("web.src.frontend.path", "/opt/scrutiny/web")
c.SetDefault("web.database.location", "/opt/scrutiny/config/scrutiny.db")

Expand Down
2 changes: 1 addition & 1 deletion webapp/backend/pkg/web/middleware/logger.go
Expand Up @@ -89,7 +89,7 @@ func LoggerMiddleware(logger logrus.FieldLogger) gin.HandlerFunc {
entry.Info(msg)
}
}
if strings.HasPrefix(path, "/api/") {
if strings.Contains(path, "/api/") {
//only debug log request/response from api endpoint.
if len(reqBody) > 0 {
entry.WithField("bodyType", "request").Debugln(reqBody) // Print request body
Expand Down
38 changes: 22 additions & 16 deletions webapp/backend/pkg/web/server.go
Expand Up @@ -27,29 +27,35 @@ func (ae *AppEngine) Setup(logger logrus.FieldLogger) *gin.Engine {
r.Use(middleware.ConfigMiddleware(ae.Config))
r.Use(gin.Recovery())

api := r.Group("/api")
basePath := ae.Config.GetString("web.listen.basepath")
logger.Debugf("basepath: %s", basePath)

base := r.Group(basePath)
{
api.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
api := base.Group("/api")
{
api.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
})
})
})
api.POST("/health/notify", handler.SendTestNotification) //check if notifications are configured correctly

api.POST("/devices/register", handler.RegisterDevices) //used by Collector to register new devices and retrieve filtered list
api.GET("/summary", handler.GetDevicesSummary) //used by Dashboard
api.GET("/summary/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown)
api.POST("/device/:wwn/smart", handler.UploadDeviceMetrics) //used by Collector to upload data
api.POST("/device/:wwn/selftest", handler.UploadDeviceSelfTests)
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
api.POST("/health/notify", handler.SendTestNotification) //check if notifications are configured correctly

api.POST("/devices/register", handler.RegisterDevices) //used by Collector to register new devices and retrieve filtered list
api.GET("/summary", handler.GetDevicesSummary) //used by Dashboard
api.GET("/summary/temp", handler.GetDevicesSummaryTempHistory) //used by Dashboard (Temperature history dropdown)
api.POST("/device/:wwn/smart", handler.UploadDeviceMetrics) //used by Collector to upload data
api.POST("/device/:wwn/selftest", handler.UploadDeviceSelfTests)
api.GET("/device/:wwn/details", handler.GetDeviceDetails) //used by Details
}
}

//Static request routing
r.StaticFS("/web", http.Dir(ae.Config.GetString("web.src.frontend.path")))
base.StaticFS("/web", http.Dir(ae.Config.GetString("web.src.frontend.path")))

//redirect base url to /web
r.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusFound, "/web")
base.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusFound, basePath+"/web")
})

//catch-all, serve index page.
Expand Down