Skip to content
This repository has been archived by the owner on Jul 16, 2021. It is now read-only.

Commit

Permalink
Adding redirects for helm client
Browse files Browse the repository at this point in the history
This will enable helm to be used in a manner like:
helm install https://monocular.example.com/charts/foo/bar

Monocular will redirect to the URL with the tarball for the chart
or the provenance file if the call to monocular ends in .prov

Signed-off-by: Matt Farina <[email protected]>
  • Loading branch information
mattfarina committed Nov 13, 2019
1 parent 87fccd8 commit 6b4a98d
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 9 deletions.
35 changes: 26 additions & 9 deletions chart/monocular/templates/ui-vhost.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ data:
server {{ template "fullname" . }}-prerender;
}
upstream chartsvc {
server {{ template "fullname" . }}-chartsvc:{{ .Values.chartsvc.service.port }};
}
server {
listen {{ .Values.ui.service.internalPort }};
Expand All @@ -21,33 +25,46 @@ data:
gzip_static on;
location / {
try_files $uri @prerender;
try_files $uri @findredirect;
}
location @prerender {
set $prerender 0;
location @findredirect {
set $findredirect 0;
# Intercept some errors, like redirects, and follow them.
proxy_intercept_errors on;
# Look for the Helm user agent. If a Helm client wants the URL we redirect
# to the file being requested.
if ($http_user_agent ~* "helm") {
set $findredirect 2;
}
# Detect bots that want HTML. We send this to a prerender service
if ($http_user_agent ~* "baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator") {
set $prerender 1;
set $findredirect 1;
}
if ($args ~ "_escaped_fragment_") {
set $prerender 1;
set $findredirect 1;
}
if ($http_user_agent ~ "Prerender") {
set $prerender 0;
set $findredirect 0;
}
if ($uri ~* "\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)") {
set $prerender 0;
set $findredirect 0;
}
if ($prerender = 1) {
if ($findredirect = 2) {
proxy_pass http://chartsvc/v1/redirect$request_uri;
}
if ($findredirect = 1) {
rewrite .* /https://$host$request_uri? break;
proxy_pass http://target_service;
}
if ($prerender = 0) {
if ($findredirect = 0) {
rewrite .* /index.html break;
}
}
Expand Down
72 changes: 72 additions & 0 deletions cmd/chartsvc/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"math"
"net/http"
"strconv"
"strings"

"github.com/globalsign/mgo/bson"
"github.com/gorilla/mux"
Expand Down Expand Up @@ -459,3 +460,74 @@ func newChartVersionListResponse(c *models.Chart) apiListResponse {

return cvl
}

func redirectToChartVersionPackage(w http.ResponseWriter, req *http.Request) {
// The URL can be in one of two forms:
// - /v1/redirect/charts/stable/aerospike
// - /v1/redirect/charts/stable/aerospike/v1.2.3
// And either of these can optionally have a trailing /

// Make sure the path is valid
ct := strings.TrimPrefix(req.URL.Path, "/v1/redirect/charts/")

// check if URL for provenance
prov := strings.HasSuffix(ct, ".prov")
ct = strings.TrimSuffix(ct, ".prov")

ct = strings.TrimSuffix(ct, "/") // Removing the optional / on the end
parts := strings.Split(ct, "/")

// Not enough parts passed in to the path
if len(parts) < 2 || len(parts) > 3 {
response.NewErrorResponse(http.StatusNotFound, "could not find chart").Write(w)
return
}

// Decide if latest or a version
var version string
if len(parts) == 3 {
version = parts[2]
}

// Look it up. This will be different if there is a version or we are getting
// the latest
db, closer := dbSession.DB()
defer closer()
var chart models.Chart
chartID := fmt.Sprintf("%s/%s", parts[0], parts[1])

if version == "" {
if err := db.C(chartCollection).FindId(chartID).One(&chart); err != nil {
log.WithError(err).Errorf("could not find chart with id %s", chartID)
response.NewErrorResponse(http.StatusNotFound, "could not find chart").Write(w)
return
}
} else {
if err := db.C(chartCollection).Find(bson.M{
"_id": chartID,
"chartversions": bson.M{"$elemMatch": bson.M{"version": version}},
}).Select(bson.M{
"name": 1, "repo": 1, "description": 1, "home": 1, "keywords": 1, "maintainers": 1, "sources": 1,
"chartversions.$": 1,
}).One(&chart); err != nil {
log.WithError(err).Errorf("could not find chart with id %s", chartID)
response.NewErrorResponse(http.StatusNotFound, "could not find chart version").Write(w)
return
}
}

// Respond with proper redirect for tarball and prov
if len(chart.ChartVersions) > 0 {
cv := chart.ChartVersions[0]
if len(cv.URLs) > 0 {
if prov {
http.Redirect(w, req, cv.URLs[0]+".prov", http.StatusTemporaryRedirect)
} else {
http.Redirect(w, req, cv.URLs[0], http.StatusTemporaryRedirect)
}
return
}
}

response.NewErrorResponse(http.StatusNotFound, "could not find chart version").Write(w)
}
86 changes: 86 additions & 0 deletions cmd/chartsvc/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -907,3 +907,89 @@ func Test_findLatestChart(t *testing.T) {
assert.Equal(t, len(data), 2, "it should return both charts")
})
}

func Test_redirectToChartVersionPackage(t *testing.T) {
tests := []struct {
name string
err error
chart models.Chart
wantCode int
location string
}{
{
"chart does not exist",
errors.New("return an error when checking if chart exists"),
models.Chart{ID: "my-repo/my-chart"},
http.StatusNotFound,
"",
},
{
"chart exists",
nil,
models.Chart{ID: "my-repo/my-chart", ChartVersions: []models.ChartVersion{{Version: "0.1.0", URLs: []string{"https://example.com/my-chart-0.1.0.tgz"}}, {Version: "0.0.1", URLs: []string{"https://example.com/my-chart-0.0.1.tgz"}}}},
http.StatusTemporaryRedirect,
"https://example.com/my-chart-0.1.0.tgz",
},
{
"chart exists with trailing /",
nil,
models.Chart{ID: "my-repo/my-chart/", ChartVersions: []models.ChartVersion{{Version: "0.1.0", URLs: []string{"https://example.com/my-chart-0.1.0.tgz"}}, {Version: "0.0.1", URLs: []string{"https://example.com/my-chart-0.0.1.tgz"}}}},
http.StatusTemporaryRedirect,
"https://example.com/my-chart-0.1.0.tgz",
},
{
"chart with version",
nil,
models.Chart{ID: "my-repo/my-chart/0.1.0", ChartVersions: []models.ChartVersion{{Version: "0.1.0", URLs: []string{"https://example.com/my-chart-0.1.0.tgz"}}}},
http.StatusTemporaryRedirect,
"https://example.com/my-chart-0.1.0.tgz",
},
{
"chart with version with trailing /",
nil,
models.Chart{ID: "my-repo/my-chart/0.1.0/", ChartVersions: []models.ChartVersion{{Version: "0.1.0", URLs: []string{"https://example.com/my-chart-0.1.0.tgz"}}}},
http.StatusTemporaryRedirect,
"https://example.com/my-chart-0.1.0.tgz",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var m mock.Mock
dbSession = mockstore.NewMockSession(&m)

if tt.err != nil {
m.On("One", mock.Anything).Return(tt.err)
} else {
m.On("One", &models.Chart{}).Return(nil).Run(func(args mock.Arguments) {
*args.Get(0).(*models.Chart) = tt.chart
})
}

w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/v1/redirect/charts/"+tt.chart.ID, nil)

redirectToChartVersionPackage(w, req)

m.AssertExpectations(t)
assert.Equal(t, tt.wantCode, w.Code)
if tt.wantCode == http.StatusTemporaryRedirect {
resp := w.Result()
assert.Equal(t, tt.location, resp.Header.Get("Location"), "response header location should be chart url")
}

// Check for provenance file
w = httptest.NewRecorder()
req = httptest.NewRequest("GET", "/v1/redirect/charts/"+tt.chart.ID+".prov", nil)

redirectToChartVersionPackage(w, req)

m.AssertExpectations(t)
assert.Equal(t, tt.wantCode, w.Code)
if tt.wantCode == http.StatusTemporaryRedirect {
resp := w.Result()
assert.Equal(t, tt.location+".prov", resp.Header.Get("Location"), "response header location should be chart provenance url")
}
})
}
}
5 changes: 5 additions & 0 deletions cmd/chartsvc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ func setupRoutes() http.Handler {
apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/versions/{version}/values.yaml").Handler(WithParams(getChartVersionValues))
apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/versions/{version}/values.schema.json").Handler(WithParams(getChartVersionSchema))

// Handle redirects to the root chart. That way you can
// `helm install monocular.example.com/charts/foo/bar` and have monocular
// redirect to the right place.
apiv1.Methods("GET").PathPrefix("/redirect").HandlerFunc(redirectToChartVersionPackage)

n := negroni.Classic()
n.UseHandler(r)
return n
Expand Down

0 comments on commit 6b4a98d

Please sign in to comment.