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

Commit 6b4a98d

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 87fccd8 commit 6b4a98d

File tree

4 files changed

+189
-9
lines changed

4 files changed

+189
-9
lines changed

chart/monocular/templates/ui-vhost.yaml

Lines changed: 26 additions & 9 deletions
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

Lines changed: 72 additions & 0 deletions
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"
@@ -459,3 +460,74 @@ func newChartVersionListResponse(c *models.Chart) apiListResponse {
459460

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

cmd/chartsvc/handler_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,3 +907,89 @@ func Test_findLatestChart(t *testing.T) {
907907
assert.Equal(t, len(data), 2, "it should return both charts")
908908
})
909909
}
910+
911+
func Test_redirectToChartVersionPackage(t *testing.T) {
912+
tests := []struct {
913+
name string
914+
err error
915+
chart models.Chart
916+
wantCode int
917+
location string
918+
}{
919+
{
920+
"chart does not exist",
921+
errors.New("return an error when checking if chart exists"),
922+
models.Chart{ID: "my-repo/my-chart"},
923+
http.StatusNotFound,
924+
"",
925+
},
926+
{
927+
"chart exists",
928+
nil,
929+
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"}}}},
930+
http.StatusTemporaryRedirect,
931+
"https://example.com/my-chart-0.1.0.tgz",
932+
},
933+
{
934+
"chart exists with trailing /",
935+
nil,
936+
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"}}}},
937+
http.StatusTemporaryRedirect,
938+
"https://example.com/my-chart-0.1.0.tgz",
939+
},
940+
{
941+
"chart with version",
942+
nil,
943+
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"}}}},
944+
http.StatusTemporaryRedirect,
945+
"https://example.com/my-chart-0.1.0.tgz",
946+
},
947+
{
948+
"chart with version with trailing /",
949+
nil,
950+
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"}}}},
951+
http.StatusTemporaryRedirect,
952+
"https://example.com/my-chart-0.1.0.tgz",
953+
},
954+
}
955+
956+
for _, tt := range tests {
957+
t.Run(tt.name, func(t *testing.T) {
958+
var m mock.Mock
959+
dbSession = mockstore.NewMockSession(&m)
960+
961+
if tt.err != nil {
962+
m.On("One", mock.Anything).Return(tt.err)
963+
} else {
964+
m.On("One", &models.Chart{}).Return(nil).Run(func(args mock.Arguments) {
965+
*args.Get(0).(*models.Chart) = tt.chart
966+
})
967+
}
968+
969+
w := httptest.NewRecorder()
970+
req := httptest.NewRequest("GET", "/v1/redirect/charts/"+tt.chart.ID, nil)
971+
972+
redirectToChartVersionPackage(w, req)
973+
974+
m.AssertExpectations(t)
975+
assert.Equal(t, tt.wantCode, w.Code)
976+
if tt.wantCode == http.StatusTemporaryRedirect {
977+
resp := w.Result()
978+
assert.Equal(t, tt.location, resp.Header.Get("Location"), "response header location should be chart url")
979+
}
980+
981+
// Check for provenance file
982+
w = httptest.NewRecorder()
983+
req = httptest.NewRequest("GET", "/v1/redirect/charts/"+tt.chart.ID+".prov", nil)
984+
985+
redirectToChartVersionPackage(w, req)
986+
987+
m.AssertExpectations(t)
988+
assert.Equal(t, tt.wantCode, w.Code)
989+
if tt.wantCode == http.StatusTemporaryRedirect {
990+
resp := w.Result()
991+
assert.Equal(t, tt.location+".prov", resp.Header.Get("Location"), "response header location should be chart provenance url")
992+
}
993+
})
994+
}
995+
}

cmd/chartsvc/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ func setupRoutes() http.Handler {
6161
apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/versions/{version}/values.yaml").Handler(WithParams(getChartVersionValues))
6262
apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/versions/{version}/values.schema.json").Handler(WithParams(getChartVersionSchema))
6363

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

0 commit comments

Comments
 (0)