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

Commit a5faf9a

Browse files
committed
Adding redirects for helm client
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]>
1 parent 9d13276 commit a5faf9a

File tree

4 files changed

+190
-9
lines changed

4 files changed

+190
-9
lines changed

chart/monocular/templates/ui-vhost.yaml

+26-9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ data:
1313
server {{ template "fullname" . }}-prerender;
1414
}
1515
16+
upstream chartsvc {
17+
server {{ template "fullname" . }}-chartsvc:{{ .Values.chartsvc.service.port }};
18+
}
19+
1620
server {
1721
listen {{ .Values.ui.service.internalPort }};
1822
@@ -21,33 +25,46 @@ data:
2125
gzip_static on;
2226
2327
location / {
24-
try_files $uri @prerender;
28+
try_files $uri @findredirect;
2529
}
2630
27-
location @prerender {
28-
set $prerender 0;
31+
location @findredirect {
32+
set $findredirect 0;
33+
34+
# Intercept some errors, like redirects, and follow them.
35+
proxy_intercept_errors on;
2936
37+
# Look for the Helm user agent. If a Helm client wants the URL we redirect
38+
# to the file being requested.
39+
if ($http_user_agent ~* "helm") {
40+
set $findredirect 2;
41+
}
42+
43+
# Detect bots that want HTML. We send this to a prerender service
3044
if ($http_user_agent ~* "baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator") {
31-
set $prerender 1;
45+
set $findredirect 1;
3246
}
3347
3448
if ($args ~ "_escaped_fragment_") {
35-
set $prerender 1;
49+
set $findredirect 1;
3650
}
3751
3852
if ($http_user_agent ~ "Prerender") {
39-
set $prerender 0;
53+
set $findredirect 0;
4054
}
4155
4256
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)") {
43-
set $prerender 0;
57+
set $findredirect 0;
4458
}
4559
46-
if ($prerender = 1) {
60+
if ($findredirect = 2) {
61+
proxy_pass http://chartsvc/v1/redirect$request_uri;
62+
}
63+
if ($findredirect = 1) {
4764
rewrite .* /https://$host$request_uri? break;
4865
proxy_pass http://target_service;
4966
}
50-
if ($prerender = 0) {
67+
if ($findredirect = 0) {
5168
rewrite .* /index.html break;
5269
}
5370
}

cmd/chartsvc/handler.go

+73
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"math"
2222
"net/http"
2323
"strconv"
24+
"strings"
2425

2526
"github.com/globalsign/mgo/bson"
2627
"github.com/gorilla/mux"
@@ -444,3 +445,75 @@ func newChartVersionListResponse(c *models.Chart) apiListResponse {
444445

445446
return cvl
446447
}
448+
449+
func handleRedirect(w http.ResponseWriter, req *http.Request) {
450+
451+
// The URL can be in one of two forms:
452+
// - /v1/redirect/charts/stable/aerospike
453+
// - /v1/redirect/charts/stable/aerospike/v1.2.3
454+
// And either of these can optionally have a trailing /
455+
456+
// Make sure the path is valid
457+
ct := strings.TrimPrefix(req.URL.Path, "/v1/redirect/charts/")
458+
459+
// check if URL for provenance
460+
prov := strings.HasSuffix(ct, ".prov")
461+
ct = strings.TrimSuffix(ct, ".prov")
462+
463+
ct = strings.TrimSuffix(ct, "/") // Removing the optional / on the end
464+
p := strings.Split(ct, "/")
465+
466+
// Not enough parts passed in to the path
467+
if len(p) < 2 || len(p) > 3 {
468+
response.NewErrorResponse(http.StatusNotFound, "could not find chart").Write(w)
469+
return
470+
}
471+
472+
// Decide if latest or a version
473+
var version string
474+
if len(p) == 3 {
475+
version = p[2]
476+
}
477+
478+
// Look it up. This will be different if there is a version or we are getting
479+
// the latest
480+
db, closer := dbSession.DB()
481+
defer closer()
482+
var chart models.Chart
483+
chartID := fmt.Sprintf("%s/%s", p[0], p[1])
484+
485+
if version == "" {
486+
if err := db.C(chartCollection).FindId(chartID).One(&chart); err != nil {
487+
log.WithError(err).Errorf("could not find chart with id %s", chartID)
488+
response.NewErrorResponse(http.StatusNotFound, "could not find chart").Write(w)
489+
return
490+
}
491+
} else {
492+
if err := db.C(chartCollection).Find(bson.M{
493+
"_id": chartID,
494+
"chartversions": bson.M{"$elemMatch": bson.M{"version": version}},
495+
}).Select(bson.M{
496+
"name": 1, "repo": 1, "description": 1, "home": 1, "keywords": 1, "maintainers": 1, "sources": 1,
497+
"chartversions.$": 1,
498+
}).One(&chart); err != nil {
499+
log.WithError(err).Errorf("could not find chart with id %s", chartID)
500+
response.NewErrorResponse(http.StatusNotFound, "could not find chart version").Write(w)
501+
return
502+
}
503+
}
504+
505+
// Respond with proper redirect for tarball and prov
506+
if len(chart.ChartVersions) > 0 {
507+
cv := chart.ChartVersions[0]
508+
if len(cv.URLs) > 0 {
509+
if prov {
510+
http.Redirect(w, req, cv.URLs[0]+".prov", http.StatusTemporaryRedirect)
511+
} else {
512+
http.Redirect(w, req, cv.URLs[0], http.StatusTemporaryRedirect)
513+
}
514+
return
515+
}
516+
}
517+
518+
response.NewErrorResponse(http.StatusNotFound, "could not find chart version").Write(w)
519+
}

cmd/chartsvc/handler_test.go

+86
Original file line numberDiff line numberDiff line change
@@ -842,3 +842,89 @@ func Test_findLatestChart(t *testing.T) {
842842
assert.Equal(t, len(data), 2, "it should return both charts")
843843
})
844844
}
845+
846+
func Test_handleRedirect(t *testing.T) {
847+
tests := []struct {
848+
name string
849+
err error
850+
chart models.Chart
851+
wantCode int
852+
location string
853+
}{
854+
{
855+
"chart does not exist",
856+
errors.New("return an error when checking if chart exists"),
857+
models.Chart{ID: "my-repo/my-chart"},
858+
http.StatusNotFound,
859+
"",
860+
},
861+
{
862+
"chart exists",
863+
nil,
864+
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"}}}},
865+
http.StatusTemporaryRedirect,
866+
"https://example.com/my-chart-0.1.0.tgz",
867+
},
868+
{
869+
"chart exists with trailing /",
870+
nil,
871+
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"}}}},
872+
http.StatusTemporaryRedirect,
873+
"https://example.com/my-chart-0.1.0.tgz",
874+
},
875+
{
876+
"chart with version",
877+
nil,
878+
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"}}}},
879+
http.StatusTemporaryRedirect,
880+
"https://example.com/my-chart-0.1.0.tgz",
881+
},
882+
{
883+
"chart with version with trailing /",
884+
nil,
885+
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"}}}},
886+
http.StatusTemporaryRedirect,
887+
"https://example.com/my-chart-0.1.0.tgz",
888+
},
889+
}
890+
891+
for _, tt := range tests {
892+
t.Run(tt.name, func(t *testing.T) {
893+
var m mock.Mock
894+
dbSession = mockstore.NewMockSession(&m)
895+
896+
if tt.err != nil {
897+
m.On("One", mock.Anything).Return(tt.err)
898+
} else {
899+
m.On("One", &models.Chart{}).Return(nil).Run(func(args mock.Arguments) {
900+
*args.Get(0).(*models.Chart) = tt.chart
901+
})
902+
}
903+
904+
w := httptest.NewRecorder()
905+
req := httptest.NewRequest("GET", "/v1/redirect/charts/"+tt.chart.ID, nil)
906+
907+
handleRedirect(w, req)
908+
909+
m.AssertExpectations(t)
910+
assert.Equal(t, tt.wantCode, w.Code)
911+
if tt.wantCode == http.StatusTemporaryRedirect {
912+
resp := w.Result()
913+
assert.Equal(t, tt.location, resp.Header.Get("Location"), "response header location should be chart url")
914+
}
915+
916+
// Check for provenance file
917+
w = httptest.NewRecorder()
918+
req = httptest.NewRequest("GET", "/v1/redirect/charts/"+tt.chart.ID+".prov", nil)
919+
920+
handleRedirect(w, req)
921+
922+
m.AssertExpectations(t)
923+
assert.Equal(t, tt.wantCode, w.Code)
924+
if tt.wantCode == http.StatusTemporaryRedirect {
925+
resp := w.Result()
926+
assert.Equal(t, tt.location+".prov", resp.Header.Get("Location"), "response header location should be chart provenance url")
927+
}
928+
})
929+
}
930+
}

cmd/chartsvc/main.go

+5
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ func setupRoutes() http.Handler {
6060
apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/versions/{version}/README.md").Handler(WithParams(getChartVersionReadme))
6161
apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/versions/{version}/values.yaml").Handler(WithParams(getChartVersionValues))
6262

63+
// Handle redirects to the root chart. That way you can
64+
// `helm install monocular.example.com/charts/foo/bar` and have monocular
65+
// redirect to the right place.
66+
apiv1.Methods("GET").PathPrefix("/redirect").HandlerFunc(handleRedirect)
67+
6368
n := negroni.Classic()
6469
n.UseHandler(r)
6570
return n

0 commit comments

Comments
 (0)