diff --git a/cmd/chart-repo/types.go b/cmd/chart-repo/types.go index 58731de54..46b9e2730 100644 --- a/cmd/chart-repo/types.go +++ b/cmd/chart-repo/types.go @@ -56,6 +56,7 @@ type chartFiles struct { ID string `bson:"_id"` Readme string Values string + Schema string Repo repo Digest string } diff --git a/cmd/chart-repo/utils.go b/cmd/chart-repo/utils.go index 7cbbdf141..c5ba0791c 100644 --- a/cmd/chart-repo/utils.go +++ b/cmd/chart-repo/utils.go @@ -430,7 +430,8 @@ func fetchAndImportFiles(dbSession datastore.Session, name string, r repo, cv ch readmeFileName := name + "/README.md" valuesFileName := name + "/values.yaml" - filenames := []string{valuesFileName, readmeFileName} + schemaFileName := name + "/values.schema.json" + filenames := []string{valuesFileName, readmeFileName, schemaFileName} files, err := extractFilesFromTarball(filenames, tarf) if err != nil { @@ -448,6 +449,11 @@ func fetchAndImportFiles(dbSession datastore.Session, name string, r repo, cv ch } else { log.WithFields(log.Fields{"name": name, "version": cv.Version}).Info("values.yaml not found") } + if v, ok := files[schemaFileName]; ok { + chartFiles.Schema = v + } else { + log.WithFields(log.Fields{"name": name, "version": cv.Version}).Info("values.schema.json not found") + } // inserts the chart files if not already indexed, or updates the existing // entry if digest has changed diff --git a/cmd/chart-repo/utils_test.go b/cmd/chart-repo/utils_test.go index b9cd4af10..58b32de36 100644 --- a/cmd/chart-repo/utils_test.go +++ b/cmd/chart-repo/utils_test.go @@ -124,10 +124,12 @@ type goodTarballClient struct { c chart skipReadme bool skipValues bool + skipSchema bool } var testChartReadme = "# readme for chart\n\nBest chart in town" var testChartValues = "image: test" +var testChartSchema = `{"properties": {}}` func (h *goodTarballClient) Do(req *http.Request) (*http.Response, error) { w := httptest.NewRecorder() @@ -139,6 +141,9 @@ func (h *goodTarballClient) Do(req *http.Request) (*http.Response, error) { if !h.skipReadme { files = append(files, tarballFile{h.c.Name + "/README.md", testChartReadme}) } + if !h.skipSchema { + files = append(files, tarballFile{h.c.Name + "/values.schema.json", testChartSchema}) + } createTestTarball(gzw, files) gzw.Flush() return w.Result(), nil @@ -159,6 +164,7 @@ func (h *authenticatedTarballClient) Do(req *http.Request) (*http.Response, erro files := []tarballFile{{h.c.Name + "/Chart.yaml", "should be a Chart.yaml here..."}} files = append(files, tarballFile{h.c.Name + "/values.yaml", testChartValues}) files = append(files, tarballFile{h.c.Name + "/README.md", testChartReadme}) + files = append(files, tarballFile{h.c.Name + "/values.schema.json", testChartSchema}) createTestTarball(gzw, files) gzw.Flush() } @@ -405,11 +411,11 @@ func Test_fetchAndImportFiles(t *testing.T) { }) t.Run("file not found", func(t *testing.T) { - netClient = &goodTarballClient{c: charts[0], skipValues: true, skipReadme: true} + netClient = &goodTarballClient{c: charts[0], skipValues: true, skipReadme: true, skipSchema: true} m := mock.Mock{} m.On("One", mock.Anything).Return(errors.New("return an error when checking if files already exists to force fetching")) chartFilesID := fmt.Sprintf("%s/%s-%s", charts[0].Repo.Name, charts[0].Name, cv.Version) - m.On("UpsertId", chartFilesID, chartFiles{chartFilesID, "", "", charts[0].Repo, cv.Digest}) + m.On("UpsertId", chartFilesID, chartFiles{chartFilesID, "", "", "", charts[0].Repo, cv.Digest}) dbSession := mockstore.NewMockSession(&m) err := fetchAndImportFiles(dbSession, charts[0].Name, charts[0].Repo, cv) assert.NoErr(t, err) @@ -421,7 +427,7 @@ func Test_fetchAndImportFiles(t *testing.T) { m := mock.Mock{} m.On("One", mock.Anything).Return(errors.New("return an error when checking if files already exists to force fetching")) chartFilesID := fmt.Sprintf("%s/%s-%s", charts[0].Repo.Name, charts[0].Name, cv.Version) - m.On("UpsertId", chartFilesID, chartFiles{chartFilesID, testChartReadme, testChartValues, charts[0].Repo, cv.Digest}) + m.On("UpsertId", chartFilesID, chartFiles{chartFilesID, testChartReadme, testChartValues, testChartSchema, charts[0].Repo, cv.Digest}) dbSession := mockstore.NewMockSession(&m) err := fetchAndImportFiles(dbSession, charts[0].Name, charts[0].Repo, cv) assert.NoErr(t, err) @@ -433,7 +439,7 @@ func Test_fetchAndImportFiles(t *testing.T) { m := mock.Mock{} m.On("One", mock.Anything).Return(errors.New("return an error when checking if files already exists to force fetching")) chartFilesID := fmt.Sprintf("%s/%s-%s", charts[0].Repo.Name, charts[0].Name, cv.Version) - m.On("UpsertId", chartFilesID, chartFiles{chartFilesID, testChartReadme, testChartValues, charts[0].Repo, cv.Digest}) + m.On("UpsertId", chartFilesID, chartFiles{chartFilesID, testChartReadme, testChartValues, testChartSchema, charts[0].Repo, cv.Digest}) dbSession := mockstore.NewMockSession(&m) err := fetchAndImportFiles(dbSession, charts[0].Name, charts[0].Repo, cv) assert.NoErr(t, err) diff --git a/cmd/chartsvc/handler.go b/cmd/chartsvc/handler.go index da7f184d8..d4eeb44f9 100644 --- a/cmd/chartsvc/handler.go +++ b/cmd/chartsvc/handler.go @@ -310,6 +310,21 @@ func getChartVersionValues(w http.ResponseWriter, req *http.Request, params Para w.Write([]byte(files.Values)) } +// getChartVersionSchema returns the values.schema.json for a given chart +func getChartVersionSchema(w http.ResponseWriter, req *http.Request, params Params) { + db, closer := dbSession.DB() + defer closer() + var files models.ChartFiles + fileID := fmt.Sprintf("%s/%s-%s", params["repo"], params["chartName"], params["version"]) + if err := db.C(filesCollection).FindId(fileID).One(&files); err != nil { + log.WithError(err).Errorf("could not find values.schema.json with id %s", fileID) + http.NotFound(w, req) + return + } + + w.Write([]byte(files.Schema)) +} + // listChartsWithFilters returns the list of repos that contains the given chart and the latest version found func listChartsWithFilters(w http.ResponseWriter, req *http.Request, params Params) { db, closer := dbSession.DB() diff --git a/cmd/chartsvc/handler_test.go b/cmd/chartsvc/handler_test.go index f9af3a371..ffa578774 100644 --- a/cmd/chartsvc/handler_test.go +++ b/cmd/chartsvc/handler_test.go @@ -47,6 +47,7 @@ var cc count const testChartReadme = "# Quickstart\n\n```bash\nhelm install my-repo/my-chart\n```" const testChartValues = "image:\n registry: docker.io\n repository: my-repo/my-chart\n tag: 0.1.0" +const testChartSchema = `{"properties": {"type": "object"}}` func iconBytes() []byte { var b bytes.Buffer @@ -730,6 +731,70 @@ func Test_getChartVersionValues(t *testing.T) { } } +func Test_getChartVersionSchema(t *testing.T) { + tests := []struct { + name string + version string + err error + files models.ChartFiles + wantCode int + }{ + { + "chart does not exist", + "0.1.0", + errors.New("return an error when checking if chart exists"), + models.ChartFiles{ID: "my-repo/my-chart"}, + http.StatusNotFound, + }, + { + "chart exists", + "3.2.1", + nil, + models.ChartFiles{ID: "my-repo/my-chart", Schema: testChartSchema}, + http.StatusOK, + }, + { + "chart does not have values.yaml", + "2.2.2", + nil, + models.ChartFiles{ID: "my-repo/my-chart"}, + http.StatusOK, + }, + } + + 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.ChartFiles{}).Return(nil).Run(func(args mock.Arguments) { + *args.Get(0).(*models.ChartFiles) = tt.files + }) + } + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/assets/"+tt.files.ID+"/versions/"+tt.version+"/values.schema.json", nil) + parts := strings.Split(tt.files.ID, "/") + params := Params{ + "repo": parts[0], + "chartName": parts[1], + "version": "0.1.0", + } + + getChartVersionSchema(w, req, params) + + m.AssertExpectations(t) + assert.Equal(t, tt.wantCode, w.Code, "http status code should match") + if tt.wantCode == http.StatusOK { + assert.Equal(t, string(w.Body.Bytes()), tt.files.Schema, "content of values.schema.json should match") + } + }) + } +} + func Test_findLatestChart(t *testing.T) { t.Run("returns mocked chart", func(t *testing.T) { chart := &models.Chart{ diff --git a/cmd/chartsvc/main.go b/cmd/chartsvc/main.go index 06450edc5..13669c4f3 100644 --- a/cmd/chartsvc/main.go +++ b/cmd/chartsvc/main.go @@ -59,6 +59,7 @@ func setupRoutes() http.Handler { apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/logo-160x160-fit.png").Handler(WithParams(getChartIcon)) apiv1.Methods("GET").Path("/assets/{repo}/{chartName}/versions/{version}/README.md").Handler(WithParams(getChartVersionReadme)) 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)) n := negroni.Classic() n.UseHandler(r) diff --git a/cmd/chartsvc/main_test.go b/cmd/chartsvc/main_test.go index c0c1c6afa..4763a9b67 100644 --- a/cmd/chartsvc/main_test.go +++ b/cmd/chartsvc/main_test.go @@ -465,3 +465,60 @@ func Test_GetChartValues(t *testing.T) { }) } } + +// tests the GET /{apiVersion}/assets/{repo}/{chartName}/versions/{version}/values/schema.json endpoint +func Test_GetChartSchema(t *testing.T) { + ts := httptest.NewServer(setupRoutes()) + defer ts.Close() + + tests := []struct { + name string + version string + err error + files models.ChartFiles + wantCode int + }{ + { + "chart does not exist", + "0.1.0", + errors.New("return an error when checking if chart exists"), + models.ChartFiles{ID: "my-repo/my-chart"}, + http.StatusNotFound, + }, + { + "chart exists", + "3.2.1", + nil, + models.ChartFiles{ID: "my-repo/my-chart", Schema: testChartSchema}, + http.StatusOK, + }, + { + "chart does not have values.schema.json", + "2.2.2", + nil, + models.ChartFiles{ID: "my-repo/my-chart"}, + http.StatusOK, + }, + } + + 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.ChartFiles{}).Return(nil).Run(func(args mock.Arguments) { + *args.Get(0).(*models.ChartFiles) = tt.files + }) + } + + res, err := http.Get(ts.URL + pathPrefix + "/assets/" + tt.files.ID + "/versions/" + tt.version + "/values.schema.json") + assert.NoError(t, err) + defer res.Body.Close() + + m.AssertExpectations(t) + assert.Equal(t, res.StatusCode, tt.wantCode, "http status code should match") + }) + } +} diff --git a/cmd/chartsvc/models/chart.go b/cmd/chartsvc/models/chart.go index 8879908cb..b0511f76e 100644 --- a/cmd/chartsvc/models/chart.go +++ b/cmd/chartsvc/models/chart.go @@ -53,6 +53,7 @@ type ChartVersion struct { URLs []string `json:"urls"` Readme string `json:"readme" bson:"-"` Values string `json:"values" bson:"-"` + Schema string `json:"schema" bson:"-"` } // ChartFiles holds the README and values for a given chart version @@ -60,4 +61,5 @@ type ChartFiles struct { ID string `bson:"_id"` Readme string Values string + Schema string }