From 87fccd89800379b909f236c38308de2a36f0c05f Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Thu, 19 Sep 2019 12:13:41 -0400 Subject: [PATCH 1/2] Make search url accessible without chartsvc in the name The search URL is going public as part of Helm. The Helm CLI calls it for search. The name of the internal service should not be part of the semi-public API in case we make changes in the future. This change makes both the old and new paths accessible via ingress. The Helm v3 betas are calling the existing path so we do not want to remove it and break their experience. Signed-off-by: Matt Farina --- chart/monocular/templates/ingress.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chart/monocular/templates/ingress.yaml b/chart/monocular/templates/ingress.yaml index 4075fc115..4469aad3e 100644 --- a/chart/monocular/templates/ingress.yaml +++ b/chart/monocular/templates/ingress.yaml @@ -21,6 +21,10 @@ spec: serviceName: {{ template "fullname" $ }}-ui servicePort: {{ $.Values.ui.service.externalPort }} path: /?(.*) + - backend: + serviceName: {{ template "fullname" $ }}-chartsvc + servicePort: {{ $.Values.chartsvc.service.port }} + path: /api/?(.*) - backend: serviceName: {{ template "fullname" $ }}-chartsvc servicePort: {{ $.Values.chartsvc.service.port }} From 6b4a98d037c10843f2efab7be8826d788a32149e Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Thu, 26 Sep 2019 15:31:04 -0400 Subject: [PATCH 2/2] 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 --- chart/monocular/templates/ui-vhost.yaml | 35 +++++++--- cmd/chartsvc/handler.go | 72 +++++++++++++++++++++ cmd/chartsvc/handler_test.go | 86 +++++++++++++++++++++++++ cmd/chartsvc/main.go | 5 ++ 4 files changed, 189 insertions(+), 9 deletions(-) diff --git a/chart/monocular/templates/ui-vhost.yaml b/chart/monocular/templates/ui-vhost.yaml index b1d720c2f..ebce0e933 100644 --- a/chart/monocular/templates/ui-vhost.yaml +++ b/chart/monocular/templates/ui-vhost.yaml @@ -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 }}; @@ -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; } } diff --git a/cmd/chartsvc/handler.go b/cmd/chartsvc/handler.go index d4eeb44f9..49454420a 100644 --- a/cmd/chartsvc/handler.go +++ b/cmd/chartsvc/handler.go @@ -21,6 +21,7 @@ import ( "math" "net/http" "strconv" + "strings" "github.com/globalsign/mgo/bson" "github.com/gorilla/mux" @@ -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) +} diff --git a/cmd/chartsvc/handler_test.go b/cmd/chartsvc/handler_test.go index ffa578774..a2986c39c 100644 --- a/cmd/chartsvc/handler_test.go +++ b/cmd/chartsvc/handler_test.go @@ -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") + } + }) + } +} diff --git a/cmd/chartsvc/main.go b/cmd/chartsvc/main.go index 13669c4f3..e2ce15c26 100644 --- a/cmd/chartsvc/main.go +++ b/cmd/chartsvc/main.go @@ -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