harbor/src/chartserver/chart_operator.go
wang yan df6e0600c9 Fix chart upload issue on event based
Use chart API to load the uploaded chart file to get the name and version

Signed-off-by: wang yan <wangyan@vmware.com>
2019-04-23 10:57:31 +08:00

265 lines
7.0 KiB
Go

package chartserver
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/Masterminds/semver"
"github.com/goharbor/harbor/src/common/models"
hlog "github.com/goharbor/harbor/src/common/utils/log"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart"
helm_repo "k8s.io/helm/pkg/repo"
)
const (
readmeFileName = "README.md"
valuesFileName = "values.yaml"
)
// ChartVersion extends the helm ChartVersion with additional labels
type ChartVersion struct {
helm_repo.ChartVersion
Labels []*models.Label `json:"labels"`
}
// ChartVersions is an array of extended ChartVersion
type ChartVersions []*ChartVersion
// ChartVersionDetails keeps the detailed data info of the chart version
type ChartVersionDetails struct {
Metadata *helm_repo.ChartVersion `json:"metadata"`
Dependencies []*chartutil.Dependency `json:"dependencies"`
Values map[string]interface{} `json:"values"`
Files map[string]string `json:"files"`
Security *SecurityReport `json:"security"`
Labels []*models.Label `json:"labels"`
}
// SecurityReport keeps the info related with security
// e.g.: digital signature, vulnerability scanning etc.
type SecurityReport struct {
Signature *DigitalSignature `json:"signature"`
}
// DigitalSignature used to indicate if the chart has been signed
type DigitalSignature struct {
Signed bool `json:"signed"`
Provenance string `json:"prov_file"`
}
// ChartInfo keeps the information of the chart
type ChartInfo struct {
Name string `json:"name"`
TotalVersions uint32 `json:"total_versions"`
LatestVersion string `json:"latest_version"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Icon string `json:"icon"`
Home string `json:"home"`
Deprecated bool `json:"deprecated"`
}
// ChartOperator is designed to process the contents of
// the specified chart version to get more details
type ChartOperator struct{}
// GetChartDetails parse the details from the provided content bytes
func (cho *ChartOperator) GetChartDetails(content []byte) (*ChartVersionDetails, error) {
chartData, err := cho.GetChartData(content)
if err != nil {
return nil, err
}
// Parse the requirements of chart
requirements, err := chartutil.LoadRequirements(chartData)
if err != nil {
// If no requirements.yaml, return empty dependency list
if _, ok := err.(chartutil.ErrNoRequirementsFile); ok {
requirements = &chartutil.Requirements{
Dependencies: make([]*chartutil.Dependency, 0),
}
} else {
return nil, err
}
}
var values map[string]interface{}
files := make(map[string]string)
// Parse values
if chartData.Values != nil {
values = parseRawValues([]byte(chartData.Values.GetRaw()))
if len(values) > 0 {
// Append values.yaml file
files[valuesFileName] = chartData.Values.Raw
}
}
// Append other files like 'README.md'
for _, v := range chartData.GetFiles() {
if v.TypeUrl == readmeFileName {
files[readmeFileName] = string(v.GetValue())
break
}
}
theChart := &ChartVersionDetails{
Dependencies: requirements.Dependencies,
Values: values,
Files: files,
}
return theChart, nil
}
// GetChartList returns a reorganized chart list
func (cho *ChartOperator) GetChartList(content []byte) ([]*ChartInfo, error) {
if content == nil || len(content) == 0 {
return nil, errors.New("zero content")
}
allCharts := make(map[string]helm_repo.ChartVersions)
if err := json.Unmarshal(content, &allCharts); err != nil {
return nil, err
}
chartList := make([]*ChartInfo, 0)
for key, chartVersions := range allCharts {
lVersion, oVersion := getTheTwoCharts(chartVersions)
if lVersion != nil && oVersion != nil {
chartInfo := &ChartInfo{
Name: key,
TotalVersions: uint32(len(chartVersions)),
}
chartInfo.Created = oVersion.Created
chartInfo.Home = lVersion.Home
chartInfo.Icon = lVersion.Icon
chartInfo.Deprecated = lVersion.Deprecated
chartInfo.LatestVersion = lVersion.GetVersion()
chartList = append(chartList, chartInfo)
}
}
// Sort the chart list by the updated time which is the create time
// of the latest version of the chart.
sort.Slice(chartList, func(i, j int) bool {
if chartList[i].Updated.Equal(chartList[j].Updated) {
return strings.Compare(chartList[i].Name, chartList[j].Name) < 0
}
return chartList[i].Updated.After(chartList[j].Updated)
})
return chartList, nil
}
// GetChartData returns raw data of chart
func (cho *ChartOperator) GetChartData(content []byte) (*chart.Chart, error) {
if content == nil || len(content) == 0 {
return nil, errors.New("zero content")
}
reader := bytes.NewReader(content)
chartData, err := chartutil.LoadArchive(reader)
if err != nil {
return nil, err
}
return chartData, nil
}
// GetChartVersions returns the chart versions
func (cho *ChartOperator) GetChartVersions(content []byte) (ChartVersions, error) {
if content == nil || len(content) == 0 {
return nil, errors.New("zero content")
}
chartVersions := make(ChartVersions, 0)
if err := json.Unmarshal(content, &chartVersions); err != nil {
return nil, err
}
return chartVersions, nil
}
// Get the latest and oldest chart versions
func getTheTwoCharts(chartVersions helm_repo.ChartVersions) (latestChart *helm_repo.ChartVersion, oldestChart *helm_repo.ChartVersion) {
if len(chartVersions) == 1 {
return chartVersions[0], chartVersions[0]
}
for _, chartVersion := range chartVersions {
currentV, err := semver.NewVersion(chartVersion.Version)
if err != nil {
// ignore it, just logged
hlog.Warningf("Malformed semversion %s for the chart %s", chartVersion.Version, chartVersion.Name)
continue
}
// Find latest chart
if latestChart == nil {
latestChart = chartVersion
} else {
lVersion, err := semver.NewVersion(latestChart.Version)
if err != nil {
// ignore it, just logged
hlog.Warningf("Malformed semversion %s for the chart %s", latestChart.Version, chartVersion.Name)
continue
}
if lVersion.LessThan(currentV) {
latestChart = chartVersion
}
}
if oldestChart == nil {
oldestChart = chartVersion
} else {
if oldestChart.Created.After(chartVersion.Created) {
oldestChart = chartVersion
}
}
}
return latestChart, oldestChart
}
// Parse the raw values to value map
func parseRawValues(rawValue []byte) map[string]interface{} {
valueMap := make(map[string]interface{})
if len(rawValue) == 0 {
return valueMap
}
values, err := chartutil.ReadValues(rawValue)
if err != nil || len(values) == 0 {
return valueMap
}
readValue(values, "", valueMap)
return valueMap
}
// Recursively read value
func readValue(values map[string]interface{}, keyPrefix string, valueMap map[string]interface{}) {
for key, value := range values {
longKey := key
if keyPrefix != "" {
longKey = fmt.Sprintf("%s.%s", keyPrefix, key)
}
if subValues, ok := value.(map[string]interface{}); ok {
readValue(subValues, longKey, valueMap)
} else {
valueMap[longKey] = value
}
}
}