mirror of
https://github.com/goharbor/harbor.git
synced 2024-06-30 16:55:07 +02:00
Use the new event model to fix bug for replicating chart triggered by event Signed-off-by: Wenkai Yin <yinw@vmware.com>
651 lines
18 KiB
Go
Executable File
651 lines
18 KiB
Go
Executable File
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/goharbor/harbor/src/controller/event/metadata"
|
|
|
|
"github.com/goharbor/harbor/src/chartserver"
|
|
"github.com/goharbor/harbor/src/common"
|
|
"github.com/goharbor/harbor/src/common/rbac"
|
|
"github.com/goharbor/harbor/src/core/config"
|
|
"github.com/goharbor/harbor/src/core/label"
|
|
hlog "github.com/goharbor/harbor/src/lib/log"
|
|
n_event "github.com/goharbor/harbor/src/pkg/notifier/event"
|
|
rep_event "github.com/goharbor/harbor/src/replication/event"
|
|
"github.com/goharbor/harbor/src/replication/model"
|
|
"github.com/goharbor/harbor/src/server/middleware/orm"
|
|
)
|
|
|
|
const (
|
|
namespaceParam = ":repo"
|
|
nameParam = ":name"
|
|
filenameParam = ":filename"
|
|
defaultRepo = "library"
|
|
rootUploadingEndpoint = "/api/chartrepo/charts"
|
|
rootIndexEndpoint = "/chartrepo/index.yaml"
|
|
chartRepoHealthEndpoint = "/api/chartrepo/health"
|
|
|
|
accessLevelPublic = iota
|
|
accessLevelRead
|
|
accessLevelWrite
|
|
accessLevelAll
|
|
accessLevelSystem
|
|
|
|
formFieldNameForChart = "chart"
|
|
formFiledNameForProv = "prov"
|
|
headerContentType = "Content-Type"
|
|
contentTypeMultipart = "multipart/form-data"
|
|
// chartPackageFileExtension is the file extension used for chart packages
|
|
chartPackageFileExtension = "tgz"
|
|
)
|
|
|
|
// chartController is a singleton instance
|
|
var chartController *chartserver.Controller
|
|
|
|
// GetChartController returns the chart controller
|
|
func GetChartController() *chartserver.Controller {
|
|
return chartController
|
|
}
|
|
|
|
// ChartRepositoryAPI provides related API handlers for the chart repository APIs
|
|
type ChartRepositoryAPI struct {
|
|
// The base controller to provide common utilities
|
|
BaseController
|
|
|
|
// For label management
|
|
labelManager *label.BaseManager
|
|
|
|
// Keep the namespace if existing
|
|
namespace string
|
|
}
|
|
|
|
// Prepare something for the following actions
|
|
func (cra *ChartRepositoryAPI) Prepare() {
|
|
// Call super prepare method
|
|
cra.BaseController.Prepare()
|
|
|
|
// Try to extract namespace for parameter of path
|
|
// It may not exist
|
|
cra.namespace = strings.TrimSpace(cra.GetStringFromPath(namespaceParam))
|
|
|
|
// Check the existence of namespace
|
|
// Exclude the following URI
|
|
// -/index.yaml
|
|
// -/api/chartserver/health
|
|
incomingURI := cra.Ctx.Request.URL.Path
|
|
if incomingURI == rootUploadingEndpoint {
|
|
// Forward to the default repository
|
|
cra.namespace = defaultRepo
|
|
}
|
|
|
|
if incomingURI != rootIndexEndpoint &&
|
|
incomingURI != chartRepoHealthEndpoint {
|
|
if !cra.requireNamespace(cra.namespace) {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Init label manager
|
|
cra.labelManager = &label.BaseManager{}
|
|
}
|
|
|
|
func (cra *ChartRepositoryAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool {
|
|
if len(subresource) == 0 {
|
|
subresource = append(subresource, rbac.ResourceHelmChart)
|
|
}
|
|
|
|
return cra.RequireProjectAccess(cra.namespace, action, subresource...)
|
|
}
|
|
|
|
// GetHealthStatus handles GET /api/chartrepo/health
|
|
func (cra *ChartRepositoryAPI) GetHealthStatus() {
|
|
// Check access
|
|
if !cra.SecurityCtx.IsAuthenticated() {
|
|
cra.SendUnAuthorizedError(errors.New("Unauthorized"))
|
|
return
|
|
}
|
|
|
|
if !cra.SecurityCtx.IsSysAdmin() {
|
|
cra.SendForbiddenError(errors.New(cra.SecurityCtx.GetUsername()))
|
|
return
|
|
}
|
|
|
|
// Directly proxy to the backend
|
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
|
}
|
|
|
|
// GetIndexByRepo handles GET /:repo/index.yaml
|
|
func (cra *ChartRepositoryAPI) GetIndexByRepo() {
|
|
// Check access
|
|
if !cra.requireAccess(rbac.ActionRead) {
|
|
return
|
|
}
|
|
|
|
// Directly proxy to the backend
|
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
|
}
|
|
|
|
// GetIndex handles GET /index.yaml
|
|
func (cra *ChartRepositoryAPI) GetIndex() {
|
|
// Check access
|
|
if !cra.SecurityCtx.IsAuthenticated() {
|
|
cra.SendUnAuthorizedError(errors.New("Unauthorized"))
|
|
return
|
|
}
|
|
|
|
if !cra.SecurityCtx.IsSysAdmin() {
|
|
cra.SendForbiddenError(errors.New(cra.SecurityCtx.GetUsername()))
|
|
return
|
|
}
|
|
|
|
results, err := cra.ProjectMgr.List(nil)
|
|
if err != nil {
|
|
cra.SendInternalServerError(err)
|
|
return
|
|
}
|
|
|
|
namespaces := []string{}
|
|
for _, r := range results.Projects {
|
|
namespaces = append(namespaces, r.Name)
|
|
}
|
|
|
|
indexFile, err := chartController.GetIndexFile(namespaces)
|
|
if err != nil {
|
|
cra.SendInternalServerError(err)
|
|
return
|
|
}
|
|
|
|
cra.WriteYamlData(indexFile)
|
|
}
|
|
|
|
// DownloadChart handles GET /:repo/charts/:filename
|
|
func (cra *ChartRepositoryAPI) DownloadChart() {
|
|
// Check access
|
|
if !cra.requireAccess(rbac.ActionRead) {
|
|
return
|
|
}
|
|
|
|
namespace := cra.GetStringFromPath(namespaceParam)
|
|
fileName := cra.GetStringFromPath(filenameParam)
|
|
// Add hook event to request context
|
|
cra.addDownloadChartEventContext(fileName, namespace, cra.Ctx.Request)
|
|
|
|
// Directly proxy to the backend
|
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
|
}
|
|
|
|
// ListCharts handles GET /api/:repo/charts
|
|
func (cra *ChartRepositoryAPI) ListCharts() {
|
|
// Check access
|
|
if !cra.requireAccess(rbac.ActionList) {
|
|
return
|
|
}
|
|
|
|
charts, err := chartController.ListCharts(cra.namespace)
|
|
if err != nil {
|
|
cra.ParseAndHandleError("fail to list charts", err)
|
|
return
|
|
}
|
|
|
|
cra.WriteJSONData(charts)
|
|
}
|
|
|
|
// ListChartVersions GET /api/:repo/charts/:name
|
|
func (cra *ChartRepositoryAPI) ListChartVersions() {
|
|
// Check access
|
|
if !cra.requireAccess(rbac.ActionList, rbac.ResourceHelmChartVersion) {
|
|
return
|
|
}
|
|
|
|
chartName := cra.GetStringFromPath(nameParam)
|
|
|
|
versions, err := chartController.GetChart(cra.namespace, chartName)
|
|
if err != nil {
|
|
cra.ParseAndHandleError("fail to get chart", err)
|
|
return
|
|
}
|
|
|
|
// Append labels
|
|
for _, chartVersion := range versions {
|
|
labels, err := cra.labelManager.GetLabelsOfResource(common.ResourceTypeChart, chartFullName(cra.namespace, chartVersion.Name, chartVersion.Version))
|
|
if err != nil {
|
|
cra.SendInternalServerError(err)
|
|
return
|
|
}
|
|
chartVersion.Labels = labels
|
|
}
|
|
|
|
cra.WriteJSONData(versions)
|
|
}
|
|
|
|
// GetChartVersion handles GET /api/:repo/charts/:name/:version
|
|
func (cra *ChartRepositoryAPI) GetChartVersion() {
|
|
// Check access
|
|
if !cra.requireAccess(rbac.ActionRead, rbac.ResourceHelmChartVersion) {
|
|
return
|
|
}
|
|
|
|
// Get other parameters
|
|
chartName := cra.GetStringFromPath(nameParam)
|
|
version := cra.GetStringFromPath(versionParam)
|
|
|
|
chartVersion, err := chartController.GetChartVersionDetails(cra.namespace, chartName, version)
|
|
if err != nil {
|
|
cra.ParseAndHandleError("fail to get chart version", err)
|
|
return
|
|
}
|
|
|
|
// Append labels
|
|
labels, err := cra.labelManager.GetLabelsOfResource(common.ResourceTypeChart, chartFullName(cra.namespace, chartName, version))
|
|
if err != nil {
|
|
cra.SendInternalServerError(err)
|
|
return
|
|
}
|
|
chartVersion.Labels = labels
|
|
|
|
cra.WriteJSONData(chartVersion)
|
|
}
|
|
|
|
// DeleteChartVersion handles DELETE /api/:repo/charts/:name/:version
|
|
func (cra *ChartRepositoryAPI) DeleteChartVersion() {
|
|
// Check access
|
|
if !cra.requireAccess(rbac.ActionDelete, rbac.ResourceHelmChartVersion) {
|
|
return
|
|
}
|
|
|
|
// Get other parameters
|
|
chartName := cra.GetStringFromPath(nameParam)
|
|
version := cra.GetStringFromPath(versionParam)
|
|
|
|
// Try to remove labels from deleting chart if existing
|
|
if err := cra.removeLabelsFromChart(chartName, version); err != nil {
|
|
cra.SendInternalServerError(err)
|
|
return
|
|
}
|
|
|
|
if err := chartController.DeleteChartVersion(cra.namespace, chartName, version); err != nil {
|
|
cra.ParseAndHandleError("fail to delete chart version", err)
|
|
return
|
|
}
|
|
|
|
event := &n_event.Event{}
|
|
metaData := &metadata.ChartDeleteMetaData{
|
|
ChartMetaData: metadata.ChartMetaData{
|
|
ProjectName: cra.namespace,
|
|
ChartName: chartName,
|
|
Versions: []string{version},
|
|
OccurAt: time.Now(),
|
|
Operator: cra.SecurityCtx.GetUsername(),
|
|
},
|
|
}
|
|
if err := event.Build(metaData); err == nil {
|
|
if err := event.Publish(); err != nil {
|
|
hlog.Errorf("failed to publish chart delete event: %v", err)
|
|
}
|
|
} else {
|
|
hlog.Errorf("failed to build chart delete event metadata: %v", err)
|
|
}
|
|
}
|
|
|
|
// UploadChartVersion handles POST /api/:repo/charts
|
|
func (cra *ChartRepositoryAPI) UploadChartVersion() {
|
|
hlog.Debugf("Header of request of uploading chart: %#v, content-len=%d", cra.Ctx.Request.Header, cra.Ctx.Request.ContentLength)
|
|
|
|
// Check access
|
|
if !cra.requireAccess(rbac.ActionCreate, rbac.ResourceHelmChartVersion) {
|
|
return
|
|
}
|
|
|
|
// Rewrite file content if the content type is "multipart/form-data"
|
|
if isMultipartFormData(cra.Ctx.Request) {
|
|
formFiles := make([]formFile, 0)
|
|
formFiles = append(formFiles,
|
|
formFile{
|
|
formField: formFieldNameForChart,
|
|
mustHave: true,
|
|
},
|
|
formFile{
|
|
formField: formFiledNameForProv,
|
|
})
|
|
if err := cra.rewriteFileContent(formFiles, cra.Ctx.Request); err != nil {
|
|
cra.SendInternalServerError(err)
|
|
return
|
|
}
|
|
if err := cra.addEventContext(formFiles, cra.Ctx.Request); err != nil {
|
|
hlog.Errorf("Failed to add chart upload context, %v", err)
|
|
}
|
|
}
|
|
|
|
// Directly proxy to the backend
|
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
|
}
|
|
|
|
// UploadChartProvFile handles POST /api/:repo/prov
|
|
func (cra *ChartRepositoryAPI) UploadChartProvFile() {
|
|
// Check access
|
|
if !cra.requireAccess(rbac.ActionCreate) {
|
|
return
|
|
}
|
|
|
|
// Rewrite file content if the content type is "multipart/form-data"
|
|
if isMultipartFormData(cra.Ctx.Request) {
|
|
formFiles := make([]formFile, 0)
|
|
formFiles = append(formFiles,
|
|
formFile{
|
|
formField: formFiledNameForProv,
|
|
mustHave: true,
|
|
})
|
|
if err := cra.rewriteFileContent(formFiles, cra.Ctx.Request); err != nil {
|
|
cra.SendInternalServerError(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Directly proxy to the backend
|
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
|
}
|
|
|
|
// DeleteChart deletes all the chart versions of the specified chart.
|
|
func (cra *ChartRepositoryAPI) DeleteChart() {
|
|
// Check access
|
|
if !cra.requireAccess(rbac.ActionDelete) {
|
|
return
|
|
}
|
|
|
|
// Get other parameters from the request
|
|
chartName := cra.GetStringFromPath(nameParam)
|
|
|
|
// Remove labels from all the deleting chart versions under the chart
|
|
chartVersions, err := chartController.GetChart(cra.namespace, chartName)
|
|
if err != nil {
|
|
cra.ParseAndHandleError("fail to get chart", err)
|
|
return
|
|
}
|
|
|
|
versions := []string{}
|
|
for _, chartVersion := range chartVersions {
|
|
versions = append(versions, chartVersion.GetVersion())
|
|
if err := cra.removeLabelsFromChart(chartName, chartVersion.GetVersion()); err != nil {
|
|
cra.SendInternalServerError(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := chartController.DeleteChart(cra.namespace, chartName); err != nil {
|
|
cra.SendInternalServerError(err)
|
|
return
|
|
}
|
|
|
|
event := &n_event.Event{}
|
|
metaData := &metadata.ChartDeleteMetaData{
|
|
ChartMetaData: metadata.ChartMetaData{
|
|
ProjectName: cra.namespace,
|
|
ChartName: chartName,
|
|
Versions: versions,
|
|
OccurAt: time.Now(),
|
|
Operator: cra.SecurityCtx.GetUsername(),
|
|
},
|
|
}
|
|
if err := event.Build(metaData); err == nil {
|
|
if err := event.Publish(); err != nil {
|
|
hlog.Errorf("failed to publish chart delete event: %v", err)
|
|
}
|
|
} else {
|
|
hlog.Errorf("failed to build chart delete event metadata: %v", err)
|
|
}
|
|
}
|
|
|
|
func (cra *ChartRepositoryAPI) removeLabelsFromChart(chartName, version string) error {
|
|
// Try to remove labels from deleting chart if existing
|
|
resourceID := chartFullName(cra.namespace, chartName, version)
|
|
labels, err := cra.labelManager.GetLabelsOfResource(common.ResourceTypeChart, resourceID)
|
|
if err == nil && len(labels) > 0 {
|
|
for _, l := range labels {
|
|
if err := cra.labelManager.RemoveLabelFromResource(common.ResourceTypeChart, resourceID, l.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Check if there exists a valid namespace
|
|
// Return true if it does
|
|
// Return false if it does not
|
|
func (cra *ChartRepositoryAPI) requireNamespace(namespace string) bool {
|
|
// Actually, never should be like this
|
|
if len(namespace) == 0 {
|
|
cra.SendBadRequestError(errors.New(":repo should be in the request URL"))
|
|
return false
|
|
}
|
|
|
|
existing, err := cra.ProjectMgr.Exists(namespace)
|
|
if err != nil {
|
|
// Check failed with error
|
|
cra.SendInternalServerError(fmt.Errorf("failed to check existence of namespace %s with error: %s", namespace, err.Error()))
|
|
return false
|
|
}
|
|
|
|
// Not existing
|
|
if !existing {
|
|
cra.SendBadRequestError(fmt.Errorf("namespace %s is not existing", namespace))
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// formFile is used to represent the uploaded files in the form
|
|
type formFile struct {
|
|
// form field key contains the form file
|
|
formField string
|
|
|
|
// flag to indicate if the file identified by the 'formField'
|
|
// must exist
|
|
mustHave bool
|
|
}
|
|
|
|
// The func is for event based chart replication policy.
|
|
// It will add a context for uploading request with key chart_upload, and consumed by upload response.
|
|
func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.Request) error {
|
|
if len(files) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, f := range files {
|
|
if f.formField == formFieldNameForChart {
|
|
mFile, _, err := cra.GetFile(f.formField)
|
|
if err != nil {
|
|
hlog.Errorf("failed to read file content for upload event, %v", err)
|
|
return err
|
|
}
|
|
var Buf bytes.Buffer
|
|
_, err = io.Copy(&Buf, mFile)
|
|
if err != nil {
|
|
hlog.Errorf("failed to copy file content for upload event, %v", err)
|
|
return err
|
|
}
|
|
chartOpr := chartserver.ChartOperator{}
|
|
chartDetails, err := chartOpr.GetChartData(Buf.Bytes())
|
|
if err != nil {
|
|
hlog.Errorf("failed to get chart content for upload event, %v", err)
|
|
return err
|
|
}
|
|
|
|
extInfo := make(map[string]interface{})
|
|
extInfo["operator"] = cra.SecurityCtx.GetUsername()
|
|
extInfo["projectName"] = cra.namespace
|
|
extInfo["chartName"] = chartDetails.Metadata.Name
|
|
|
|
public, err := cra.ProjectMgr.IsPublic(cra.namespace)
|
|
if err != nil {
|
|
hlog.Errorf("failed to check the public of project %s: %v", cra.namespace, err)
|
|
public = false
|
|
}
|
|
e := &rep_event.Event{
|
|
Type: rep_event.EventTypeChartUpload,
|
|
Resource: &model.Resource{
|
|
Type: model.ResourceTypeChart,
|
|
Metadata: &model.ResourceMetadata{
|
|
Repository: &model.Repository{
|
|
Name: fmt.Sprintf("%s/%s", cra.namespace, chartDetails.Metadata.Name),
|
|
Metadata: map[string]interface{}{
|
|
"public": strconv.FormatBool(public),
|
|
},
|
|
},
|
|
Artifacts: []*model.Artifact{
|
|
{
|
|
Tags: []string{chartDetails.Metadata.Version},
|
|
},
|
|
},
|
|
},
|
|
ExtendedInfo: extInfo,
|
|
},
|
|
}
|
|
*request = *(request.WithContext(context.WithValue(request.Context(), common.ChartUploadCtxKey, e)))
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (cra *ChartRepositoryAPI) addDownloadChartEventContext(fileName, namespace string, request *http.Request) {
|
|
chartName, version := parseChartVersionFromFilename(fileName)
|
|
event := &metadata.ChartDownloadMetaData{
|
|
ChartMetaData: metadata.ChartMetaData{
|
|
ProjectName: namespace,
|
|
ChartName: chartName,
|
|
Versions: []string{version},
|
|
OccurAt: time.Now(),
|
|
Operator: cra.SecurityCtx.GetUsername(),
|
|
},
|
|
}
|
|
*request = *(request.WithContext(context.WithValue(request.Context(), common.ChartDownloadCtxKey, event)))
|
|
}
|
|
|
|
// If the files are uploaded with multipart/form-data mimetype, beego will extract the data
|
|
// from the request automatically. Then the request passed to the backend server with proxying
|
|
// way will have empty content.
|
|
// This method will refill the requests with file content.
|
|
func (cra *ChartRepositoryAPI) rewriteFileContent(files []formFile, request *http.Request) error {
|
|
if len(files) == 0 {
|
|
return nil // no files, early return
|
|
}
|
|
|
|
var body bytes.Buffer
|
|
w := multipart.NewWriter(&body)
|
|
defer func() {
|
|
if err := w.Close(); err != nil {
|
|
// Just log it
|
|
hlog.Errorf("Failed to defer close multipart writer with error: %s", err.Error())
|
|
}
|
|
}()
|
|
|
|
// Process files by key one by one
|
|
for _, f := range files {
|
|
mFile, mHeader, err := cra.GetFile(f.formField)
|
|
|
|
// Handle error case by case
|
|
if err != nil {
|
|
formatedErr := fmt.Errorf("Get file content with multipart header from key '%s' failed with error: %s", f.formField, err.Error())
|
|
if f.mustHave || err != http.ErrMissingFile {
|
|
return formatedErr
|
|
}
|
|
|
|
// Error can be ignored, just log it
|
|
hlog.Warning(formatedErr.Error())
|
|
continue
|
|
}
|
|
|
|
fw, err := w.CreateFormFile(f.formField, mHeader.Filename)
|
|
if err != nil {
|
|
return fmt.Errorf("Create form file with multipart header failed with error: %s", err.Error())
|
|
}
|
|
|
|
_, err = io.Copy(fw, mFile)
|
|
if err != nil {
|
|
return fmt.Errorf("Copy file stream in multipart form data failed with error: %s", err.Error())
|
|
}
|
|
|
|
}
|
|
|
|
request.Header.Set(headerContentType, w.FormDataContentType())
|
|
request.ContentLength = -1
|
|
request.Body = ioutil.NopCloser(&body)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Initialize the chart service controller
|
|
func initializeChartController() (*chartserver.Controller, error) {
|
|
addr, err := config.GetChartMuseumEndpoint()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to get the endpoint URL of chart storage server: %s", err.Error())
|
|
}
|
|
|
|
addr = strings.TrimSuffix(addr, "/")
|
|
url, err := url.Parse(addr)
|
|
if err != nil {
|
|
return nil, errors.New("Endpoint URL of chart storage server is malformed")
|
|
}
|
|
|
|
controller, err := chartserver.NewController(url, orm.Middleware())
|
|
if err != nil {
|
|
return nil, errors.New("Failed to initialize chart API controller")
|
|
}
|
|
|
|
hlog.Debugf("Chart storage server is set to %s", url.String())
|
|
hlog.Info("API controller for chart repository server is successfully initialized")
|
|
|
|
return controller, nil
|
|
}
|
|
|
|
// Check if the request content type is "multipart/form-data"
|
|
func isMultipartFormData(req *http.Request) bool {
|
|
return strings.Contains(req.Header.Get(headerContentType), contentTypeMultipart)
|
|
}
|
|
|
|
// Return the chart full name
|
|
func chartFullName(namespace, chartName, version string) string {
|
|
if strings.HasPrefix(chartName, "http") {
|
|
return fmt.Sprintf("%s:%s", chartName, version)
|
|
}
|
|
return fmt.Sprintf("%s/%s:%s", namespace, chartName, version)
|
|
}
|
|
|
|
// parseChartVersionFromFilename parse chart and version from file name
|
|
func parseChartVersionFromFilename(filename string) (string, string) {
|
|
noExt := strings.TrimSuffix(path.Base(filename), fmt.Sprintf(".%s", chartPackageFileExtension))
|
|
parts := strings.Split(noExt, "-")
|
|
name := parts[0]
|
|
version := ""
|
|
for idx, part := range parts[1:] {
|
|
if _, err := strconv.Atoi(string(part[0])); err == nil { // see if this part looks like a version (starts w int)
|
|
version = strings.Join(parts[idx+1:], "-")
|
|
break
|
|
}
|
|
name = fmt.Sprintf("%s-%s", name, part)
|
|
}
|
|
if version == "" { // no parts looked like a real version, just take everything after last hyphen
|
|
lastIndex := len(parts) - 1
|
|
name = strings.Join(parts[:lastIndex], "-")
|
|
version = parts[lastIndex]
|
|
}
|
|
return name, version
|
|
}
|