src/jetstream/plugins/monocular/chart_svc.go
package monocular
import (
"errors"
"fmt"
"net/http"
"os"
"path"
"github.com/cloudfoundry-incubator/stratos/src/jetstream/plugins/monocular/store"
"github.com/labstack/echo/v4"
log "github.com/sirupsen/logrus"
)
// Functions to provide a Monocular compatible API with our chart store
// List all helm Charts - gets the latest version for each Chart
func (m *Monocular) listCharts(c echo.Context) error {
log.Debug("List Charts called")
// Check if this is a request for Artifact Hub
if handled, err := m.handleArtifactRequest(c, m.fetchChartsFromArtifactHub); handled {
return err
}
charts, err := m.ChartStore.GetLatestCharts()
if err != nil {
return err
}
// Translate the list into an array of Charts
var list APIListResponse
for _, chart := range charts {
list = append(list, m.translateToChartAPIResponse(chart, nil))
}
meta := Meta{
TotalPages: 1,
}
body := BodyAPIListResponse{
Data: &list,
Meta: meta,
}
return c.JSON(200, body)
}
func (m *Monocular) getHelmRepositoryURL(chart *store.ChartStoreRecord) (string, error) {
endpoint, err := m.portalProxy.GetCNSIRecord(chart.EndpointID)
if err != nil {
return "", nil
}
return endpoint.APIEndpoint.String(), nil
}
// Get chart and ensure the chart URL is an absolute URL with scheme
func (m *Monocular) getChartFromStore(repo, name, version string) (*store.ChartStoreRecord, error) {
chart, err := m.ChartStore.GetChart(repo, name, version)
if err != nil {
return nil, err
}
if urlDoesNotContainSchema(chart.ChartURL) {
// Relative URL
repoURL, err := m.getHelmRepositoryURL(chart)
if err != nil {
return chart, err
}
chart.ChartURL = joinURL(repoURL, chart.ChartURL)
}
return chart, nil
}
// Get the latest version of a given chart
func (m *Monocular) getChart(c echo.Context) error {
log.Debug("Get Chart called")
// Check if this is a request for Artifact Hub
if handled, err := m.handleArtifactRequest(c, m.artifactHubGetChart); handled {
return err
}
repo := c.Param("repo")
chartName := c.Param("name")
chart, err := m.getChartFromStore(repo, chartName, "")
if err != nil {
return err
}
chartYaml := m.getChartYaml(*chart)
body := BodyAPIResponse{
Data: *m.translateToChartAPIResponse(chart, chartYaml),
}
return c.JSON(200, body)
}
func (m *Monocular) getIcon(c echo.Context) error {
log.Debug("Get Icon called")
// Process ArtifactHub request
if handled, err := m.handleArtifactRequest(c, m.artifactHubGetIconHandler); handled {
return err
}
repo := c.Param("repo")
chartName := c.Param("chartName")
version := c.Param("version")
if len(version) == 0 {
log.Debugf("Get icon for %s/%s", repo, chartName)
} else {
log.Debugf("Get icon for %s/%s-%s", repo, chartName, version)
}
chart, err := m.getChartFromStore(repo, chartName, version)
if err != nil {
log.Error("Can not find chart")
return errors.New("Error")
}
// This will download and cache the icon if it is not already cached - it returns the local file path to the icon file
// or an empty string if no icon is available or could not be downloaded
iconFilePath, _ := m.cacheChartIcon(*chart)
if len(iconFilePath) == 0 {
// No icon or error downloading
http.Redirect(c.Response().Writer, c.Request(), "/core/assets/custom/placeholder.png", http.StatusTemporaryRedirect)
return nil
}
c.File(iconFilePath)
return nil
}
// /chartsvc/v1/charts/:repo/:name/versions/:version
// Get specific chart version
func (m *Monocular) getChartVersion(c echo.Context) error {
log.Debug("getChartAndVersion called")
// Process ArtifactHub request
if handled, err := m.handleArtifactRequest(c, m.artifactHubGetChartVersion); handled {
return err
}
repo := c.Param("repo")
chartName := c.Param("name")
version := c.Param("version")
chart, err := m.ChartStore.GetChart(repo, chartName, version)
if err != nil {
return err
}
chartYaml := m.getChartYaml(*chart)
if chartYaml == nil {
// Error - could not get chart yaml
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Can not find Chart.yaml for %s/%s-%s", repo, chartName, version))
}
body := BodyAPIResponse{
Data: *m.translateToChartVersionAPIResponse(chart, chartYaml),
}
return c.JSON(200, body)
}
// /chartsvc/v1/charts/:repo/:name/versions
// Get all chart versions for a given chart
func (m *Monocular) getChartVersions(c echo.Context) error {
// Check if this is a request for Artifact Hub
var err error
externalMonocularEndpoint, err := m.isExternalMonocularRequest(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
repo := c.Param("repo")
chartName := c.Param("name")
var charts []*store.ChartStoreRecord
if externalMonocularEndpoint != nil {
charts, err = m.artifactHubGetChartVersions(c, externalMonocularEndpoint.GUID, repo, chartName)
} else {
// Get all versions for a given chart
charts, err = m.ChartStore.GetChartVersions(repo, chartName)
}
if err != nil {
return err
}
// Translate the list into an array of Charts
var list APIListResponse
for _, chart := range charts {
list = append(list, m.translateToChartVersionAPIResponse(chart, nil))
}
body := BodyAPIListResponse{
Data: &list,
}
return c.JSON(200, body)
}
// Get a file such as the README or valyes for a given chart version
func (m *Monocular) getChartAndVersionFile(c echo.Context) error {
log.Debug("Get Chart file called")
repo := c.Param("repo")
chartName := c.Param("name")
version := c.Param("version")
filename := c.Param("filename")
if !isPermittedFile(filename) {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Can not find file %s for the specified chart", filename))
}
log.Debugf("Get chart file: %s", filename)
chart, err := m.getChartFromStore(repo, chartName, version)
if err != nil {
return err
}
if m.cacheChart(*chart) == nil {
return c.File(path.Join(m.getChartCacheFolder(*chart), filename))
}
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Can not find file %s for the specified chart", filename))
}
func (m *Monocular) getChartValues(c echo.Context) error {
endpointID := c.Param("endpoint")
repo := c.Param("repo")
chartName := c.Param("name")
version := c.Param("version")
// Built in Monocular
if endpointID == "default" {
filename := "values.yaml"
log.Debugf("Get chart file: %s", filename)
chart, err := m.getChartFromStore(repo, chartName, version)
if err != nil {
return err
}
if m.cacheChart(*chart) == nil {
return c.File(path.Join(m.getChartCacheFolder(*chart), filename))
}
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Can not find file %s for the specified chart", filename))
}
// Artifact Hub
return m.artifactHubGetChartFileNamed(c, "values.yaml")
}
// This is the simpler version that returns just enough data needed for the Charts list view
// This is a slight cheat - the response is not as complete as the Monocular API, but includes
// enough for the UI and saves us having to pull out all of the Chart.yaml files
func (m *Monocular) translateToChartAPIResponse(record *store.ChartStoreRecord, chartYaml *ChartMetadata) *APIResponse {
response := &APIResponse{
ID: fmt.Sprintf("%s/%s", record.Repository, record.Name),
Type: "chart",
Relationships: make(map[string]Rel),
Attributes: m.translateToChart(record, chartYaml),
}
response.Relationships["latestChartVersion"] = Rel{
Data: m.translateToChartVersion(record, chartYaml),
}
return response
}
func (m *Monocular) translateToChart(record *store.ChartStoreRecord, chartYaml *ChartMetadata) Chart {
chart := Chart{
Name: record.Name,
Description: record.Description,
Repo: Repo{},
Icon: fmt.Sprintf("/v1/assets/%s/%s/%s/logo", record.Repository, record.Name, record.Version),
Sources: record.Sources,
}
chart.Repo.Name = record.Repository
if chartYaml != nil {
chart.Keywords = chartYaml.Keywords
// Prefer the Chart Yaml description if we have it (db one might be truncated)
chart.Description = chartYaml.Description
chart.Maintainers = make([]ChartMaintainer, len(chartYaml.Maintainers))
for index, maintainer := range chartYaml.Maintainers {
chart.Maintainers[index] = *maintainer
}
chart.Home = chartYaml.Home
}
return chart
}
func (m *Monocular) translateToChartVersion(record *store.ChartStoreRecord, chartYaml *ChartMetadata) ChartVersion {
chartVersion := ChartVersion{
Version: record.Version,
AppVersion: record.AppVersion,
Digest: record.Digest,
Created: record.Created,
URLs: make([]string, 1),
}
chartVersion.URLs[0] = record.ChartURL
if chartYaml != nil {
// If we have the Chart yaml, then we already have the chart
// Add in the files that are available
cacheFolder := m.getChartCacheFolder(*record)
chartVersion.Readme = getFileAssetURL(record.Repository, record.Name, record.Version, cacheFolder, "README.md")
chartVersion.Schema = getFileAssetURL(record.Repository, record.Name, record.Version, cacheFolder, "values.schema.json")
chartVersion.Values = getFileAssetURL(record.Repository, record.Name, record.Version, cacheFolder, "values.yaml")
}
return chartVersion
}
func (m *Monocular) translateToChartVersionAPIResponse(record *store.ChartStoreRecord, chartYaml *ChartMetadata) *APIResponse {
response := &APIResponse{
ID: fmt.Sprintf("%s/%s-%s", record.Repository, record.Name, record.Version),
Type: "chartVersion",
Relationships: make(map[string]Rel),
Attributes: m.translateToChartVersion(record, chartYaml),
}
response.Relationships["chart"] = Rel{
Data: m.translateToChart(record, chartYaml),
}
return response
}
func getFileAssetURL(repo, name, version, folder, filename string) string {
cachePath := path.Join(folder, filename)
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
return ""
}
return fmt.Sprintf("/v1/assets/%s/%s/versions/%s/%s", repo, name, version, filename)
}