mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-15 23:05:57 +01:00
Refactor the chart service implementation to provide more extemding flexibilities
- use unified proxy handler to handle all the proxy traffic - define interfaces for the handlers - implement the interface at chart controller level - refactor UT cases - update other related reference code Signed-off-by: Steven Zou <szou@vmware.com>
This commit is contained in:
parent
8250bce9c5
commit
df241c0666
@ -1,17 +0,0 @@
|
|||||||
package chartserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BaseHandler defines the handlers related with the chart server itself.
|
|
||||||
type BaseHandler struct {
|
|
||||||
// Proxy used to to transfer the traffic of requests
|
|
||||||
// It's mainly used to talk to the backend chart server
|
|
||||||
trafficProxy *ProxyEngine
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHealthStatus will return the health status of the backend chart repository server
|
|
||||||
func (bh *BaseHandler) GetHealthStatus(w http.ResponseWriter, req *http.Request) {
|
|
||||||
bh.trafficProxy.ServeHTTP(w, req)
|
|
||||||
}
|
|
@ -1,22 +1,14 @@
|
|||||||
package chartserver
|
package chartserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/ui/filter"
|
|
||||||
"github.com/goharbor/harbor/src/ui/promgr/metamgr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The backend server
|
// The backend server handler
|
||||||
var mockServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
var mockHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.RequestURI {
|
switch r.RequestURI {
|
||||||
case "/health":
|
case "/health":
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
@ -52,13 +44,27 @@ var mockServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.Respon
|
|||||||
w.Write([]byte(harborChartV))
|
w.Write([]byte(harborChartV))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if r.Method == http.MethodDelete {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
case "/api/repo1/charts/harbor/1.0.0":
|
case "/api/repo1/charts/harbor/1.0.0":
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
w.Write([]byte("Internal error"))
|
w.Write([]byte("Not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "/api/repo1/charts/harbor/0.2.1":
|
||||||
|
if r.Method == http.MethodDelete {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "/api/repo1/charts/harbor":
|
case "/api/repo1/charts/harbor":
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
w.Write([]byte(chartVersionsOfHarbor))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "/repo3/charts/harbor-0.8.1.tgz":
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
w.Write([]byte("Unauthorized"))
|
w.Write([]byte("Unauthorized"))
|
||||||
@ -68,164 +74,99 @@ var mockServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.Respon
|
|||||||
|
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
w.Write([]byte("not supported"))
|
w.Write([]byte("not supported"))
|
||||||
}))
|
})
|
||||||
|
|
||||||
// Chart controller reference
|
func createMockObjects() (*httptest.Server, *Controller, error) {
|
||||||
var mockController *Controller
|
s := httptest.NewServer(mockHandler)
|
||||||
|
backendURL, err := url.Parse(s.URL)
|
||||||
// The frontend server
|
if err != nil {
|
||||||
var frontServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
s.Close()
|
||||||
if r.RequestURI == "/health" && r.Method == http.MethodGet {
|
return nil, nil, err
|
||||||
mockController.GetBaseHandler().GetHealthStatus(w, r)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.RequestURI == "/index.yaml" && r.Method == http.MethodGet {
|
mockController, err := NewController(backendURL)
|
||||||
mockProjectMgr := &mockProjectManager{}
|
if err != nil {
|
||||||
*r = *(r.WithContext(context.WithValue(r.Context(), filter.PmKey, mockProjectMgr)))
|
s.Close()
|
||||||
mockController.GetRepositoryHandler().GetIndexFile(w, r)
|
return nil, nil, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var indexYaml = regexp.MustCompile(`^/\w+/index.yaml$`)
|
return s, mockController, nil
|
||||||
if r.Method == http.MethodGet && indexYaml.MatchString(r.RequestURI) {
|
}
|
||||||
mockController.GetRepositoryHandler().GetIndexFileWithNS(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var downloadFile = regexp.MustCompile(`^/\w+/charts/.+$`)
|
|
||||||
if r.Method == http.MethodGet && downloadFile.MatchString(r.RequestURI) {
|
|
||||||
mockController.GetRepositoryHandler().DownloadChartObject(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var listCharts = regexp.MustCompile(`^/api/\w+/charts$`)
|
|
||||||
if r.Method == http.MethodGet && listCharts.MatchString(r.RequestURI) {
|
|
||||||
mockController.GetManipulationHandler().ListCharts(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var getChart = regexp.MustCompile(`^/api/\w+/charts/[\w-]+$`)
|
|
||||||
if r.Method == http.MethodGet && getChart.MatchString(r.RequestURI) {
|
|
||||||
mockController.GetManipulationHandler().GetChart(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var getChartV = regexp.MustCompile(`^/api/\w+/charts/.+/.+$`)
|
|
||||||
if r.Method == http.MethodGet && getChartV.MatchString(r.RequestURI) {
|
|
||||||
*r = *(r.WithContext(context.WithValue(r.Context(), NamespaceContextKey, "repo1")))
|
|
||||||
mockController.GetManipulationHandler().GetChartVersion(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var postCharts = regexp.MustCompile(`^/api/\w+/charts$`)
|
|
||||||
if r.Method == http.MethodPost && postCharts.MatchString(r.RequestURI) {
|
|
||||||
mockController.GetManipulationHandler().UploadChartVersion(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var postProv = regexp.MustCompile(`^/api/\w+/prov$`)
|
|
||||||
if r.Method == http.MethodPost && postProv.MatchString(r.RequestURI) {
|
|
||||||
mockController.GetManipulationHandler().UploadProvenanceFile(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var deleteChartV = regexp.MustCompile(`^/api/\w+/charts/.+/.+$`)
|
|
||||||
if r.Method == http.MethodDelete && deleteChartV.MatchString(r.RequestURI) {
|
|
||||||
mockController.GetManipulationHandler().DeleteChartVersion(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
|
||||||
w.Write([]byte(fmt.Sprintf("no handler to handle %s", r.RequestURI)))
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Http client
|
// Http client
|
||||||
var httpClient = NewChartClient(nil)
|
var httpClient = NewChartClient(nil)
|
||||||
|
|
||||||
// Mock project manager
|
|
||||||
type mockProjectManager struct{}
|
|
||||||
|
|
||||||
func (mpm *mockProjectManager) Get(projectIDOrName interface{}) (*models.Project, error) {
|
|
||||||
return nil, errors.New("Not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mpm *mockProjectManager) Create(*models.Project) (int64, error) {
|
|
||||||
return -1, errors.New("Not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mpm *mockProjectManager) Delete(projectIDOrName interface{}) error {
|
|
||||||
return errors.New("Not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mpm *mockProjectManager) Update(projectIDOrName interface{}, project *models.Project) error {
|
|
||||||
return errors.New("Not implemented")
|
|
||||||
}
|
|
||||||
func (mpm *mockProjectManager) List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error) {
|
|
||||||
results := &models.ProjectQueryResult{
|
|
||||||
Total: 2,
|
|
||||||
Projects: make([]*models.Project, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
results.Projects = append(results.Projects, &models.Project{ProjectID: 0, Name: "repo1"})
|
|
||||||
results.Projects = append(results.Projects, &models.Project{ProjectID: 1, Name: "repo2"})
|
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mpm *mockProjectManager) IsPublic(projectIDOrName interface{}) (bool, error) {
|
|
||||||
return false, errors.New("Not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mpm *mockProjectManager) Exists(projectIDOrName interface{}) (bool, error) {
|
|
||||||
return false, errors.New("Not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all public project
|
|
||||||
func (mpm *mockProjectManager) GetPublic() ([]*models.Project, error) {
|
|
||||||
return nil, errors.New("Not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the project manager uses a metadata manager, return it, otherwise return nil
|
|
||||||
func (mpm *mockProjectManager) GetMetadataManager() metamgr.ProjectMetadataManager {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility methods
|
|
||||||
// Start the mock frontend and backend servers
|
|
||||||
func startMockServers() error {
|
|
||||||
mockServer.Start()
|
|
||||||
backendURL, err := url.Parse(getTheAddrOfMockServer())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mockController, err = NewController(backendURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
frontServer.Start()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the mock frontend and backend servers
|
|
||||||
func stopMockServers() {
|
|
||||||
frontServer.Close()
|
|
||||||
mockServer.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the address of the backend mock server
|
|
||||||
func getTheAddrOfMockServer() string {
|
|
||||||
return mockServer.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the address of the frontend server
|
|
||||||
func getTheAddrOfFrontServer() string {
|
|
||||||
return frontServer.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock content
|
// Mock content
|
||||||
|
var chartVersionsOfHarbor = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "harbor",
|
||||||
|
"home": "https://github.com/vmware/harbor",
|
||||||
|
"sources": [
|
||||||
|
"https://github.com/vmware/harbor/tree/master/contrib/helm/harbor"
|
||||||
|
],
|
||||||
|
"version": "0.2.1",
|
||||||
|
"description": "An Enterprise-class Docker Registry by VMware",
|
||||||
|
"keywords": [
|
||||||
|
"vmware",
|
||||||
|
"docker",
|
||||||
|
"registry",
|
||||||
|
"harbor"
|
||||||
|
],
|
||||||
|
"maintainers": [
|
||||||
|
{
|
||||||
|
"name": "Jesse Hu",
|
||||||
|
"email": "huh@vmware.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paulczar",
|
||||||
|
"email": "username.taken@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engine": "gotpl",
|
||||||
|
"icon": "https://raw.githubusercontent.com/vmware/harbor/master/docs/img/harbor_logo.png",
|
||||||
|
"appVersion": "1.5.0",
|
||||||
|
"urls": [
|
||||||
|
"charts/harbor-0.2.1.tgz"
|
||||||
|
],
|
||||||
|
"created": "2018-08-29T10:26:29.625749155Z",
|
||||||
|
"digest": "2538edf4ddb797af8e025f3bd6226270440110bbdb689bad48656a519a154236"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "harbor",
|
||||||
|
"home": "https://github.com/vmware/harbor",
|
||||||
|
"sources": [
|
||||||
|
"https://github.com/vmware/harbor/tree/master/contrib/helm/harbor"
|
||||||
|
],
|
||||||
|
"version": "0.2.0",
|
||||||
|
"description": "An Enterprise-class Docker Registry by VMware",
|
||||||
|
"keywords": [
|
||||||
|
"vmware",
|
||||||
|
"docker",
|
||||||
|
"registry",
|
||||||
|
"harbor"
|
||||||
|
],
|
||||||
|
"maintainers": [
|
||||||
|
{
|
||||||
|
"name": "Jesse Hu",
|
||||||
|
"email": "huh@vmware.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paulczar",
|
||||||
|
"email": "username.taken@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engine": "gotpl",
|
||||||
|
"icon": "https://raw.githubusercontent.com/vmware/harbor/master/docs/img/harbor_logo.png",
|
||||||
|
"appVersion": "1.5.0",
|
||||||
|
"urls": [
|
||||||
|
"charts/harbor-0.2.0.tgz"
|
||||||
|
],
|
||||||
|
"created": "2018-08-29T10:26:21.141611102Z",
|
||||||
|
"digest": "fc8aae8dade9f0dfca12e9f1085081c49843d30a063a3fa7eb42497e3ceb277c"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`
|
||||||
var harborChartV = `
|
var harborChartV = `
|
||||||
{
|
{
|
||||||
"name": "harbor",
|
"name": "harbor",
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Masterminds/semver"
|
"github.com/Masterminds/semver"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"k8s.io/helm/pkg/chartutil"
|
"k8s.io/helm/pkg/chartutil"
|
||||||
helm_repo "k8s.io/helm/pkg/repo"
|
helm_repo "k8s.io/helm/pkg/repo"
|
||||||
@ -21,6 +22,15 @@ const (
|
|||||||
valuesFileName = "values.yaml"
|
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
|
// ChartVersionDetails keeps the detailed data info of the chart version
|
||||||
type ChartVersionDetails struct {
|
type ChartVersionDetails struct {
|
||||||
Metadata *helm_repo.ChartVersion `json:"metadata"`
|
Metadata *helm_repo.ChartVersion `json:"metadata"`
|
||||||
@ -28,6 +38,7 @@ type ChartVersionDetails struct {
|
|||||||
Values map[string]interface{} `json:"values"`
|
Values map[string]interface{} `json:"values"`
|
||||||
Files map[string]string `json:"files"`
|
Files map[string]string `json:"files"`
|
||||||
Security *SecurityReport `json:"security"`
|
Security *SecurityReport `json:"security"`
|
||||||
|
Labels []*models.Label `json:"labels"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecurityReport keeps the info related with security
|
// SecurityReport keeps the info related with security
|
||||||
@ -154,12 +165,12 @@ func (cho *ChartOperator) GetChartList(content []byte) ([]*ChartInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetChartVersions returns the chart versions
|
// GetChartVersions returns the chart versions
|
||||||
func (cho *ChartOperator) GetChartVersions(content []byte) (helm_repo.ChartVersions, error) {
|
func (cho *ChartOperator) GetChartVersions(content []byte) (ChartVersions, error) {
|
||||||
if content == nil || len(content) == 0 {
|
if content == nil || len(content) == 0 {
|
||||||
return nil, errors.New("zero content")
|
return nil, errors.New("zero content")
|
||||||
}
|
}
|
||||||
|
|
||||||
chartVersions := make(helm_repo.ChartVersions, 0)
|
chartVersions := make(ChartVersions, 0)
|
||||||
if err := json.Unmarshal(content, &chartVersions); err != nil {
|
if err := json.Unmarshal(content, &chartVersions); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
||||||
)
|
)
|
||||||
@ -25,20 +24,21 @@ type Credential struct {
|
|||||||
// A reverse proxy will be created and managed to proxy the related traffics between API and
|
// A reverse proxy will be created and managed to proxy the related traffics between API and
|
||||||
// backend chart server
|
// backend chart server
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
|
// Proxy used to to transfer the traffic of requests
|
||||||
|
// It's mainly used to talk to the backend chart server
|
||||||
|
trafficProxy *ProxyEngine
|
||||||
|
|
||||||
|
// Parse and process the chart version to provide required info data
|
||||||
|
chartOperator *ChartOperator
|
||||||
|
|
||||||
|
// HTTP client used to call the realted APIs of the backend chart repositories
|
||||||
|
apiClient *ChartClient
|
||||||
|
|
||||||
// The access endpoint of the backend chart repository server
|
// The access endpoint of the backend chart repository server
|
||||||
backendServerAddr *url.URL
|
backendServerAddress *url.URL
|
||||||
|
|
||||||
// To cover the server info and status requests
|
// Cache the chart data
|
||||||
baseHandler *BaseHandler
|
chartCache *ChartCache
|
||||||
|
|
||||||
// To cover the chart repository requests
|
|
||||||
repositoryHandler *RepositoryHandler
|
|
||||||
|
|
||||||
// To cover all the manipulation requests
|
|
||||||
manipulationHandler *ManipulationHandler
|
|
||||||
|
|
||||||
// To cover the other utility requests
|
|
||||||
utilityHandler *UtilityHandler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewController is constructor of the chartserver.Controller
|
// NewController is constructor of the chartserver.Controller
|
||||||
@ -53,15 +53,6 @@ func NewController(backendServer *url.URL) (*Controller, error) {
|
|||||||
Password: os.Getenv(passwordKey),
|
Password: os.Getenv(passwordKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use customized reverse proxy
|
|
||||||
proxy := NewProxyEngine(backendServer, cred)
|
|
||||||
|
|
||||||
// Create http client with customized timeouts
|
|
||||||
client := NewChartClient(cred)
|
|
||||||
|
|
||||||
// Initialize chart operator for use
|
|
||||||
operator := &ChartOperator{}
|
|
||||||
|
|
||||||
// Creat cache
|
// Creat cache
|
||||||
cacheCfg, err := getCacheConfig()
|
cacheCfg, err := getCacheConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -75,79 +66,18 @@ func NewController(backendServer *url.URL) (*Controller, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Controller{
|
return &Controller{
|
||||||
backendServerAddr: backendServer,
|
backendServerAddress: backendServer,
|
||||||
baseHandler: &BaseHandler{proxy},
|
// Use customized reverse proxy
|
||||||
repositoryHandler: &RepositoryHandler{
|
trafficProxy: NewProxyEngine(backendServer, cred),
|
||||||
trafficProxy: proxy,
|
// Initialize chart operator for use
|
||||||
apiClient: client,
|
chartOperator: &ChartOperator{},
|
||||||
backendServerAddress: backendServer,
|
// Create http client with customized timeouts
|
||||||
},
|
apiClient: NewChartClient(cred),
|
||||||
manipulationHandler: &ManipulationHandler{
|
chartCache: cache,
|
||||||
trafficProxy: proxy,
|
|
||||||
chartOperator: operator,
|
|
||||||
apiClient: client,
|
|
||||||
backendServerAddress: backendServer,
|
|
||||||
chartCache: cache,
|
|
||||||
},
|
|
||||||
utilityHandler: &UtilityHandler{
|
|
||||||
apiClient: client,
|
|
||||||
backendServerAddress: backendServer,
|
|
||||||
chartOperator: operator,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBaseHandler returns the reference of BaseHandler
|
// APIPrefix returns the API prefix path of calling backend chart service.
|
||||||
func (c *Controller) GetBaseHandler() *BaseHandler {
|
func (c *Controller) APIPrefix(namespace string) string {
|
||||||
return c.baseHandler
|
return fmt.Sprintf("%s/api/%s/charts", c.backendServerAddress.String(), namespace)
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepositoryHandler returns the reference of RepositoryHandler
|
|
||||||
func (c *Controller) GetRepositoryHandler() *RepositoryHandler {
|
|
||||||
return c.repositoryHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetManipulationHandler returns the reference of ManipulationHandler
|
|
||||||
func (c *Controller) GetManipulationHandler() *ManipulationHandler {
|
|
||||||
return c.manipulationHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUtilityHandler returns the reference of UtilityHandler
|
|
||||||
func (c *Controller) GetUtilityHandler() *UtilityHandler {
|
|
||||||
return c.utilityHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
// What's the cache driver if it is set
|
|
||||||
func parseCacheDriver() (string, bool) {
|
|
||||||
driver, ok := os.LookupEnv(cacheDriverENVKey)
|
|
||||||
return strings.ToLower(driver), ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and parse the configuration for the chart cache
|
|
||||||
func getCacheConfig() (*ChartCacheConfig, error) {
|
|
||||||
driver, isSet := parseCacheDriver()
|
|
||||||
if !isSet {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if driver != cacheDriverMem && driver != cacheDriverRedis {
|
|
||||||
return nil, fmt.Errorf("cache driver '%s' is not supported, only support 'memory' and 'redis'", driver)
|
|
||||||
}
|
|
||||||
|
|
||||||
if driver == cacheDriverMem {
|
|
||||||
return &ChartCacheConfig{
|
|
||||||
DriverType: driver,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
redisConfigV := os.Getenv(redisENVKey)
|
|
||||||
redisCfg, err := parseRedisConfig(redisConfigV)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse redis configurations from '%s' with error: %s", redisCfg, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ChartCacheConfig{
|
|
||||||
DriverType: driver,
|
|
||||||
Config: redisCfg,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
@ -1,225 +1,21 @@
|
|||||||
package chartserver
|
package chartserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ghodss/yaml"
|
|
||||||
helm_repo "k8s.io/helm/pkg/repo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Prepare, start the mock servers
|
// Test controller
|
||||||
func TestStartServers(t *testing.T) {
|
func TestController(t *testing.T) {
|
||||||
if err := startMockServers(); err != nil {
|
s, c, err := createMockObjects()
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test /health
|
|
||||||
func TestGetHealthOfBaseHandler(t *testing.T) {
|
|
||||||
content, err := httpClient.GetContent(fmt.Sprintf("%s/health", getTheAddrOfFrontServer()))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
status := make(map[string]interface{})
|
prefix := c.APIPrefix("fake")
|
||||||
if err := json.Unmarshal(content, &status); err != nil {
|
expected := fmt.Sprintf("%s/api/%s/charts", s.URL, "fake")
|
||||||
t.Fatalf("Unmarshal error: %s, %s", err, content)
|
if prefix != expected {
|
||||||
}
|
t.Fatalf("expect '%s' but got '%s'", expected, prefix)
|
||||||
healthy, ok := status["health"].(bool)
|
|
||||||
if !ok || !healthy {
|
|
||||||
t.Fatalf("Expect healthy of server to be 'true' but got %v", status["health"])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get /repo1/index.yaml
|
|
||||||
func TestGetIndexYamlByRepo(t *testing.T) {
|
|
||||||
indexFile, err := getIndexYaml("/repo1/index.yaml")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(indexFile.Entries) != 3 {
|
|
||||||
t.Fatalf("Expect index file with 3 entries, but got %d", len(indexFile.Entries))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test get /index.yaml
|
|
||||||
func TestGetUnifiedYamlFile(t *testing.T) {
|
|
||||||
indexFile, err := getIndexYaml("/index.yaml")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(indexFile.Entries) != 5 {
|
|
||||||
t.Fatalf("Expect index file with 5 entries, but got %d", len(indexFile.Entries))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok := indexFile.Entries["repo1/harbor"]
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Expect chart entry 'repo1/harbor' but got nothing")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok = indexFile.Entries["repo2/harbor"]
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Expect chart entry 'repo2/harbor' but got nothing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test download /:repo/charts/chart.tar
|
|
||||||
// Use this case to test the proxy function
|
|
||||||
func TestDownloadChart(t *testing.T) {
|
|
||||||
content, err := httpClient.GetContent(fmt.Sprintf("%s/repo1/charts/harbor-0.2.0.tgz", getTheAddrOfFrontServer()))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gotSize := len(content)
|
|
||||||
expectSize := len(helmChartContent)
|
|
||||||
|
|
||||||
if gotSize != expectSize {
|
|
||||||
t.Fatalf("Expect %d bytes data but got %d bytes", expectSize, gotSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test get /api/:repo/charts
|
|
||||||
func TestRetrieveChartList(t *testing.T) {
|
|
||||||
content, err := httpClient.GetContent(fmt.Sprintf("%s/api/repo1/charts", getTheAddrOfFrontServer()))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
chartList := make([]*ChartInfo, 0)
|
|
||||||
err = json.Unmarshal(content, &chartList)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unmarshal error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(chartList) != 2 {
|
|
||||||
t.Fatalf("Expect to get 2 charts in the list but got %d", len(chartList))
|
|
||||||
}
|
|
||||||
|
|
||||||
foundItem := false
|
|
||||||
for _, chartInfo := range chartList {
|
|
||||||
if chartInfo.Name == "hello-helm" && chartInfo.TotalVersions == 2 {
|
|
||||||
foundItem = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundItem {
|
|
||||||
t.Fatalf("Expect chart 'hello-helm' with 2 versions but got nothing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test get /api/:repo/charts/:chart_name/:version
|
|
||||||
func TestGetChartVersion(t *testing.T) {
|
|
||||||
content, err := httpClient.GetContent(fmt.Sprintf("%s/api/repo1/charts/harbor/0.2.0", getTheAddrOfFrontServer()))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
chartVersion := &ChartVersionDetails{}
|
|
||||||
if err = json.Unmarshal(content, chartVersion); err != nil {
|
|
||||||
t.Fatalf("Unmarshal error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if chartVersion.Metadata.Name != "harbor" {
|
|
||||||
t.Fatalf("Expect harbor chart version but got %s", chartVersion.Metadata.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if chartVersion.Metadata.Version != "0.2.0" {
|
|
||||||
t.Fatalf("Expect version '0.2.0' but got version %s", chartVersion.Metadata.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(chartVersion.Dependencies) != 1 {
|
|
||||||
t.Fatalf("Expect 1 dependency but got %d ones", len(chartVersion.Dependencies))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(chartVersion.Values) != 99 {
|
|
||||||
t.Fatalf("Expect 99 k-v values but got %d", len(chartVersion.Values))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test get /api/:repo/charts/:chart_name/:version with none-existing version
|
|
||||||
func TestGetChartVersionWithError(t *testing.T) {
|
|
||||||
_, err := httpClient.GetContent(fmt.Sprintf("%s/api/repo1/charts/harbor/1.0.0", getTheAddrOfFrontServer()))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expect an error but got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get /api/repo1/charts/harbor
|
|
||||||
// 401 will be rewritten to 500 with specified error
|
|
||||||
func TestResponseRewrite(t *testing.T) {
|
|
||||||
response, err := http.Get(fmt.Sprintf("%s/api/repo1/charts/harbor", getTheAddrOfFrontServer()))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.StatusCode != http.StatusInternalServerError {
|
|
||||||
t.Fatalf("Expect status code 500 but got %d", response.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := ioutil.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Read bytes from http response failed with error: %s", err)
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
errObj := make(map[string]interface{})
|
|
||||||
if err = json.Unmarshal(bytes, &errObj); err != nil {
|
|
||||||
t.Fatalf("Unmarshal error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg, ok := errObj["error"]; !ok {
|
|
||||||
t.Fatal("Expect an error message from server but got nothing")
|
|
||||||
} else {
|
|
||||||
if !strings.Contains(msg.(string), "operation request from unauthorized source is rejected") {
|
|
||||||
t.Fatal("Missing the required error message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the chart searching
|
|
||||||
func TestChartSearching(t *testing.T) {
|
|
||||||
namespaces := []string{"repo1", "repo2"}
|
|
||||||
q := "harbor"
|
|
||||||
|
|
||||||
results, err := mockController.GetRepositoryHandler().SearchChart(q, namespaces)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expect nil error but got '%s'", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) != 2 {
|
|
||||||
t.Fatalf("expect 2 results but got %d", len(results))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear environments
|
|
||||||
func TestStopServers(t *testing.T) {
|
|
||||||
stopMockServers()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility method for getting index yaml file
|
|
||||||
func getIndexYaml(path string) (*helm_repo.IndexFile, error) {
|
|
||||||
content, err := httpClient.GetContent(fmt.Sprintf("%s%s", getTheAddrOfFrontServer(), path))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
indexFile := &helm_repo.IndexFile{}
|
|
||||||
if err := yaml.Unmarshal(content, indexFile); err != nil {
|
|
||||||
return nil, fmt.Errorf("Unmarshal error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if indexFile == nil {
|
|
||||||
return nil, fmt.Errorf("Got nil index yaml file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return indexFile, nil
|
|
||||||
}
|
|
||||||
|
89
src/chartserver/handler_interface.go
Normal file
89
src/chartserver/handler_interface.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package chartserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"k8s.io/helm/cmd/helm/search"
|
||||||
|
helm_repo "k8s.io/helm/pkg/repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceHandler defines the related methods to handle kinds of chart service requests.
|
||||||
|
type ServiceHandler interface {
|
||||||
|
// ListCharts lists all the charts under the specified namespace.
|
||||||
|
//
|
||||||
|
// namespace string: the chart namespace.
|
||||||
|
//
|
||||||
|
// If succeed, a chart info list with nil error will be returned;
|
||||||
|
// otherwise, a non-nil error will be got.
|
||||||
|
ListCharts(namespace string) ([]*ChartInfo, error)
|
||||||
|
|
||||||
|
// Get all the chart versions of the specified chart under the namespace.
|
||||||
|
//
|
||||||
|
// namespace string: the chart namespace.
|
||||||
|
// chartName string: the name of the chart, e.g: "harbor"
|
||||||
|
//
|
||||||
|
// If succeed, a chart version list with nil error will be returned;
|
||||||
|
// otherwise, a non-nil error will be got.
|
||||||
|
GetChart(namespace, chartName string) (helm_repo.ChartVersions, error)
|
||||||
|
|
||||||
|
// Get the detailed info of the specified chart version under the namespace.
|
||||||
|
// The detailed info includes chart summary, dependencies, values and signature status etc.
|
||||||
|
//
|
||||||
|
// namespace string: the chart namespace.
|
||||||
|
// chartName string: the name of the chart, e.g: "harbor"
|
||||||
|
// version string: the SemVer version of the chart, e.g: "0.2.0"
|
||||||
|
//
|
||||||
|
// If succeed, chart version details with nil error will be returned;
|
||||||
|
// otherwise, a non-nil error will be got.
|
||||||
|
GetChartVersionDetails(namespace, chartName, version string) (*ChartVersionDetails, error)
|
||||||
|
|
||||||
|
// SearchChart search charts in the specified namespaces with the keyword q.
|
||||||
|
// RegExp mode is enabled as default.
|
||||||
|
// For each chart, only the latest version will shown in the result list if matched to avoid duplicated entries.
|
||||||
|
// Keep consistent with `helm search` command.
|
||||||
|
//
|
||||||
|
// q string : the searching keyword
|
||||||
|
// namespaces []string : the search namespace scope
|
||||||
|
//
|
||||||
|
// If succeed, a search result list with nil error will be returned;
|
||||||
|
// otherwise, a non-nil error will be got.
|
||||||
|
SearchChart(q string, namespaces []string) ([]*search.Result, error)
|
||||||
|
|
||||||
|
// GetIndexFile will read the index.yaml under all namespaces and merge them as a single one
|
||||||
|
// Please be aware that, to support this function, the backend chart repository server should
|
||||||
|
// enable multi-tenancies
|
||||||
|
//
|
||||||
|
// namespaces []string : all the namespaces with accessing permissions
|
||||||
|
//
|
||||||
|
// If succeed, a unified merged index file with nil error will be returned;
|
||||||
|
// otherwise, a non-nil error will be got.
|
||||||
|
GetIndexFile(namespaces []string) (*helm_repo.IndexFile, error)
|
||||||
|
|
||||||
|
// Get the chart summary of the specified chart version.
|
||||||
|
//
|
||||||
|
// namespace string: the chart namespace.
|
||||||
|
// chartName string: the name of the chart, e.g: "harbor"
|
||||||
|
// version string: the SemVer version of the chart, e.g: "0.2.0"
|
||||||
|
//
|
||||||
|
// If succeed, chart version summary with nil error will be returned;
|
||||||
|
// otherwise, a non-nil error will be got.
|
||||||
|
GetChartVersion(namespace, name, version string) (*helm_repo.ChartVersion, error)
|
||||||
|
|
||||||
|
// DeleteChart deletes all the chart versions of the specified chart under the namespace.
|
||||||
|
//
|
||||||
|
// namespace string: the chart namespace.
|
||||||
|
// chartName string: the name of the chart, e.g: "harbor"
|
||||||
|
//
|
||||||
|
// If succeed, a nil error will be returned;
|
||||||
|
// otherwise, a non-nil error will be got.
|
||||||
|
DeleteChart(namespace, chartName string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyTrafficHandler defines the handler methods to handle the proxy traffic.
|
||||||
|
type ProxyTrafficHandler interface {
|
||||||
|
// Proxy the traffic to the backended server
|
||||||
|
//
|
||||||
|
// Req *http.Request : The incoming http request
|
||||||
|
// w http.ResponseWriter : The response writer reference
|
||||||
|
ProxyTraffic(w http.ResponseWriter, req *http.Request)
|
||||||
|
}
|
93
src/chartserver/handler_manipulation.go
Normal file
93
src/chartserver/handler_manipulation.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package chartserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
|
helm_repo "k8s.io/helm/pkg/repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListCharts gets the chart list under the namespace
|
||||||
|
// See @ServiceHandler.ListCharts
|
||||||
|
func (c *Controller) ListCharts(namespace string) ([]*ChartInfo, error) {
|
||||||
|
if len(strings.TrimSpace(namespace)) == 0 {
|
||||||
|
return nil, errors.New("empty namespace when getting chart list")
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := c.apiClient.GetContent(c.APIPrefix(namespace))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.chartOperator.GetChartList(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChart returns all the chart versions under the specified chart
|
||||||
|
// See @ServiceHandler.GetChart
|
||||||
|
func (c *Controller) GetChart(namespace, chartName string) (ChartVersions, error) {
|
||||||
|
if len(namespace) == 0 {
|
||||||
|
return nil, errors.New("empty name when getting chart versions")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chartName) == 0 {
|
||||||
|
return nil, errors.New("no chart name specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/%s", c.APIPrefix(namespace), chartName)
|
||||||
|
data, err := c.apiClient.GetContent(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
versions := make(ChartVersions, 0)
|
||||||
|
if err := json.Unmarshal(data, &versions); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteChartVersion will delete the specified version of the chart
|
||||||
|
// See @ServiceHandler.DeleteChartVersion
|
||||||
|
func (c *Controller) DeleteChartVersion(namespace, chartName, version string) error {
|
||||||
|
if len(namespace) == 0 {
|
||||||
|
return errors.New("empty namespace when deleting chart version")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chartName) == 0 || len(version) == 0 {
|
||||||
|
return errors.New("invalid chart for deleting")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/%s/%s", c.APIPrefix(namespace), chartName, version)
|
||||||
|
|
||||||
|
return c.apiClient.DeleteContent(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChartVersion returns the summary of the specified chart version.
|
||||||
|
// See @ServiceHandler.GetChartVersion
|
||||||
|
func (c *Controller) GetChartVersion(namespace, name, version string) (*helm_repo.ChartVersion, error) {
|
||||||
|
if len(namespace) == 0 {
|
||||||
|
return nil, errors.New("empty namespace when getting summary of chart version")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(name) == 0 || len(version) == 0 {
|
||||||
|
return nil, errors.New("invalid chart when getting summary")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/%s/%s", c.APIPrefix(namespace), name, version)
|
||||||
|
|
||||||
|
content, err := c.apiClient.GetContent(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
chartVersion := &helm_repo.ChartVersion{}
|
||||||
|
if err := yaml.Unmarshal(content, chartVersion); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return chartVersion, nil
|
||||||
|
}
|
88
src/chartserver/handler_manipulation_test.go
Normal file
88
src/chartserver/handler_manipulation_test.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package chartserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test get /api/:repo/charts/harbor
|
||||||
|
func TestGetChart(t *testing.T) {
|
||||||
|
s, c, err := createMockObjects()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
versions, err := c.GetChart("repo1", "harbor")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(versions) != 2 {
|
||||||
|
t.Fatalf("expect 2 chart versions of harbor but got %d", len(versions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test delete /api/:repo/charts/harbor/0.2.0
|
||||||
|
func TestDeleteChartVersion(t *testing.T) {
|
||||||
|
s, c, err := createMockObjects()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
if err := c.DeleteChartVersion("repo1", "harbor", "0.2.0"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test get /api/:repo/charts
|
||||||
|
func TestRetrieveChartList(t *testing.T) {
|
||||||
|
s, c, err := createMockObjects()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
chartList, err := c.ListCharts("repo1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chartList) != 2 {
|
||||||
|
t.Fatalf("Expect to get 2 charts in the list but got %d", len(chartList))
|
||||||
|
}
|
||||||
|
|
||||||
|
foundItem := false
|
||||||
|
for _, chartInfo := range chartList {
|
||||||
|
if chartInfo.Name == "hello-helm" && chartInfo.TotalVersions == 2 {
|
||||||
|
foundItem = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundItem {
|
||||||
|
t.Fatalf("Expect chart 'hello-helm' with 2 versions but got nothing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the GetChartVersion in utility handler
|
||||||
|
func TestGetChartVersionSummary(t *testing.T) {
|
||||||
|
s, c, err := createMockObjects()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
chartV, err := c.GetChartVersion("repo1", "harbor", "0.2.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if chartV.GetName() != "harbor" {
|
||||||
|
t.Fatalf("expect chart name 'harbor' but got '%s'", chartV.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
if chartV.GetVersion() != "0.2.0" {
|
||||||
|
t.Fatalf("expect chart version '0.2.0' but got '%s'", chartV.GetVersion())
|
||||||
|
}
|
||||||
|
}
|
12
src/chartserver/handler_proxy_traffic.go
Normal file
12
src/chartserver/handler_proxy_traffic.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package chartserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProxyTraffic implements the interface method.
|
||||||
|
func (c *Controller) ProxyTraffic(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if c.trafficProxy != nil {
|
||||||
|
c.trafficProxy.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
}
|
136
src/chartserver/handler_proxy_traffic_test.go
Normal file
136
src/chartserver/handler_proxy_traffic_test.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package chartserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
|
helm_repo "k8s.io/helm/pkg/repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The frontend server
|
||||||
|
var frontServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mockController.ProxyTraffic(w, r)
|
||||||
|
}))
|
||||||
|
|
||||||
|
var mockServer *httptest.Server
|
||||||
|
var mockController *Controller
|
||||||
|
|
||||||
|
// Prepare case
|
||||||
|
func TestStartMockServers(t *testing.T) {
|
||||||
|
s, c, err := createMockObjects()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mockController = c
|
||||||
|
mockServer = s
|
||||||
|
|
||||||
|
frontServer.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test /health
|
||||||
|
func TestGetHealthOfBaseHandler(t *testing.T) {
|
||||||
|
content, err := httpClient.GetContent(fmt.Sprintf("%s/api/chartrepo/health", frontServer.URL))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := make(map[string]interface{})
|
||||||
|
if err := json.Unmarshal(content, &status); err != nil {
|
||||||
|
t.Fatalf("Unmarshal error: %s, %s", err, content)
|
||||||
|
}
|
||||||
|
healthy, ok := status["health"].(bool)
|
||||||
|
if !ok || !healthy {
|
||||||
|
t.Fatalf("Expect healthy of server to be 'true' but got %v", status["health"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get /repo1/index.yaml
|
||||||
|
func TestGetIndexYamlByRepo(t *testing.T) {
|
||||||
|
indexFile, err := getIndexYaml("/chartrepo/repo1/index.yaml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(indexFile.Entries) != 3 {
|
||||||
|
t.Fatalf("Expect index file with 3 entries, but got %d", len(indexFile.Entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test download /:repo/charts/chart.tar
|
||||||
|
// Use this case to test the proxy function
|
||||||
|
func TestDownloadChart(t *testing.T) {
|
||||||
|
content, err := httpClient.GetContent(fmt.Sprintf("%s/chartrepo/repo1/charts/harbor-0.2.0.tgz", frontServer.URL))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotSize := len(content)
|
||||||
|
expectSize := len(helmChartContent)
|
||||||
|
|
||||||
|
if gotSize != expectSize {
|
||||||
|
t.Fatalf("Expect %d bytes data but got %d bytes", expectSize, gotSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get /api/repo1/charts/harbor
|
||||||
|
// 401 will be rewritten to 500 with specified error
|
||||||
|
func TestResponseRewrite(t *testing.T) {
|
||||||
|
response, err := http.Get(fmt.Sprintf("%s/chartrepo/repo3/charts/harbor-0.8.1.tgz", frontServer.URL))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusInternalServerError {
|
||||||
|
t.Fatalf("Expect status code 500 but got %d", response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Read bytes from http response failed with error: %s", err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
errObj := make(map[string]interface{})
|
||||||
|
if err = json.Unmarshal(bytes, &errObj); err != nil {
|
||||||
|
t.Fatalf("Unmarshal error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg, ok := errObj["error"]; !ok {
|
||||||
|
t.Fatal("Expect an error message from server but got nothing")
|
||||||
|
} else {
|
||||||
|
if !strings.Contains(msg.(string), "operation request from unauthorized source is rejected") {
|
||||||
|
t.Fatal("Missing the required error message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear env
|
||||||
|
func TestStopMockServers(t *testing.T) {
|
||||||
|
frontServer.Close()
|
||||||
|
mockServer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility method for getting index yaml file
|
||||||
|
func getIndexYaml(path string) (*helm_repo.IndexFile, error) {
|
||||||
|
content, err := httpClient.GetContent(fmt.Sprintf("%s%s", frontServer.URL, path))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
indexFile := &helm_repo.IndexFile{}
|
||||||
|
if err := yaml.Unmarshal(content, indexFile); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unmarshal error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if indexFile == nil {
|
||||||
|
return nil, fmt.Errorf("Got nil index yaml file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexFile, nil
|
||||||
|
}
|
@ -1,17 +1,12 @@
|
|||||||
package chartserver
|
package chartserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ghodss/yaml"
|
"github.com/ghodss/yaml"
|
||||||
"github.com/goharbor/harbor/src/ui/filter"
|
|
||||||
"k8s.io/helm/cmd/helm/search"
|
|
||||||
helm_repo "k8s.io/helm/pkg/repo"
|
helm_repo "k8s.io/helm/pkg/repo"
|
||||||
|
|
||||||
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
||||||
@ -24,124 +19,33 @@ const (
|
|||||||
searchMaxScore = 25
|
searchMaxScore = 25
|
||||||
)
|
)
|
||||||
|
|
||||||
// RepositoryHandler defines all the handlers to handle the requests related with chart repository
|
|
||||||
// e.g: index.yaml and downloading chart objects
|
|
||||||
type RepositoryHandler struct {
|
|
||||||
// Proxy used to to transfer the traffic of requests
|
|
||||||
// It's mainly used to talk to the backend chart server
|
|
||||||
trafficProxy *ProxyEngine
|
|
||||||
|
|
||||||
// HTTP client used to call the realted APIs of the backend chart repositories
|
|
||||||
apiClient *ChartClient
|
|
||||||
|
|
||||||
// Point to the url of the backend server
|
|
||||||
backendServerAddress *url.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Result returned by worker
|
// Result returned by worker
|
||||||
type processedResult struct {
|
type processedResult struct {
|
||||||
namespace string
|
namespace string
|
||||||
indexFileOfRepo *helm_repo.IndexFile
|
indexFileOfRepo *helm_repo.IndexFile
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIndexFileWithNS will read the index.yaml data under the specified namespace
|
|
||||||
func (rh *RepositoryHandler) GetIndexFileWithNS(w http.ResponseWriter, req *http.Request) {
|
|
||||||
rh.trafficProxy.ServeHTTP(w, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIndexFile will read the index.yaml under all namespaces and merge them as a single one
|
// GetIndexFile will read the index.yaml under all namespaces and merge them as a single one
|
||||||
// Please be aware that, to support this function, the backend chart repository server should
|
// Please be aware that, to support this function, the backend chart repository server should
|
||||||
// enable multi-tenancies
|
// enable multi-tenancies
|
||||||
func (rh *RepositoryHandler) GetIndexFile(w http.ResponseWriter, req *http.Request) {
|
//
|
||||||
// Get project manager references
|
// See @ServiceHandler.GetIndexFile
|
||||||
projectMgr, err := filter.GetProjectManager(req)
|
func (c *Controller) GetIndexFile(namespaces []string) (*helm_repo.IndexFile, error) {
|
||||||
if err != nil {
|
if namespaces == nil || len(namespaces) == 0 {
|
||||||
WriteInternalError(w, err)
|
return emptyIndexFile(), nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all the projects
|
return c.getIndexYaml(namespaces)
|
||||||
results, err := projectMgr.List(nil)
|
|
||||||
if err != nil {
|
|
||||||
WriteInternalError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no projects existing, return empty index.yaml content immediately
|
|
||||||
if results.Total == 0 {
|
|
||||||
w.Write(emptyIndexFile())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
namespaces := []string{}
|
|
||||||
for _, p := range results.Projects {
|
|
||||||
namespaces = append(namespaces, p.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
mergedIndexFile, err := rh.getIndexYaml(namespaces)
|
|
||||||
if err != nil {
|
|
||||||
WriteInternalError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := yaml.Marshal(mergedIndexFile)
|
|
||||||
if err != nil {
|
|
||||||
WriteInternalError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DownloadChartObject will download the stored chart object to the client
|
|
||||||
// e.g: helm install
|
|
||||||
func (rh *RepositoryHandler) DownloadChartObject(w http.ResponseWriter, req *http.Request) {
|
|
||||||
rh.trafficProxy.ServeHTTP(w, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchChart search charts in the specified namespaces with the keyword q.
|
|
||||||
// RegExp mode is enabled as default.
|
|
||||||
// For each chart, only the latest version will shown in the result list if matched to avoid duplicated entries.
|
|
||||||
// Keep consistent with `helm search` command.
|
|
||||||
func (rh *RepositoryHandler) SearchChart(q string, namespaces []string) ([]*search.Result, error) {
|
|
||||||
if len(q) == 0 || len(namespaces) == 0 {
|
|
||||||
// Return empty list
|
|
||||||
return []*search.Result{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the merged index yaml file of the namespaces
|
|
||||||
ind, err := rh.getIndexYaml(namespaces)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the search index
|
|
||||||
index := search.NewIndex()
|
|
||||||
// As the repo name is already merged into the index yaml, we use empty repo name.
|
|
||||||
// Set 'All' to false to return only one version for each chart.
|
|
||||||
index.AddRepo("", ind, false)
|
|
||||||
|
|
||||||
// Search
|
|
||||||
// RegExp is enabled
|
|
||||||
results, err := index.Search(q, searchMaxScore, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort results.
|
|
||||||
search.SortScore(results)
|
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getIndexYaml will get the index yaml files for all the namespaces and merge them
|
// getIndexYaml will get the index yaml files for all the namespaces and merge them
|
||||||
// as one unified index yaml file.
|
// as one unified index yaml file.
|
||||||
func (rh *RepositoryHandler) getIndexYaml(namespaces []string) (*helm_repo.IndexFile, error) {
|
func (c *Controller) getIndexYaml(namespaces []string) (*helm_repo.IndexFile, error) {
|
||||||
// The final merged index file
|
// The final merged index file
|
||||||
mergedIndexFile := &helm_repo.IndexFile{
|
mergedIndexFile := &helm_repo.IndexFile{
|
||||||
APIVersion: "v1",
|
APIVersion: "v1",
|
||||||
Entries: make(map[string]helm_repo.ChartVersions),
|
Entries: make(map[string]helm_repo.ChartVersions),
|
||||||
Generated: time.Now(),
|
Generated: time.Now().Round(time.Second),
|
||||||
PublicKeys: []string{},
|
PublicKeys: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +79,7 @@ func (rh *RepositoryHandler) getIndexYaml(namespaces []string) (*helm_repo.Index
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
for res := range resultChan {
|
for res := range resultChan {
|
||||||
rh.mergeIndexFile(res.namespace, mergedIndexFile, res.indexFileOfRepo)
|
c.mergeIndexFile(res.namespace, mergedIndexFile, res.indexFileOfRepo)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -203,7 +107,7 @@ LOOP:
|
|||||||
workerPool <- struct{}{}
|
workerPool <- struct{}{}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
indexFile, err := rh.getIndexYamlWithNS(ns)
|
indexFile, err := c.getIndexYamlWithNS(ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(errorChan) == 0 {
|
if len(errorChan) == 0 {
|
||||||
//Only need one error as failure signal
|
//Only need one error as failure signal
|
||||||
@ -254,13 +158,13 @@ LOOP:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the index yaml file under the specified namespace from the backend server
|
// Get the index yaml file under the specified namespace from the backend server
|
||||||
func (rh *RepositoryHandler) getIndexYamlWithNS(namespace string) (*helm_repo.IndexFile, error) {
|
func (c *Controller) getIndexYamlWithNS(namespace string) (*helm_repo.IndexFile, error) {
|
||||||
// Join url path
|
// Join url path
|
||||||
url := path.Join(namespace, "index.yaml")
|
url := path.Join(namespace, "index.yaml")
|
||||||
url = fmt.Sprintf("%s/%s", rh.backendServerAddress.String(), url)
|
url = fmt.Sprintf("%s/%s", c.backendServerAddress.String(), url)
|
||||||
hlog.Debugf("Getting index.yaml from '%s'", url)
|
hlog.Debugf("Getting index.yaml from '%s'", url)
|
||||||
|
|
||||||
content, err := rh.apiClient.GetContent(url)
|
content, err := c.apiClient.GetContent(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -276,7 +180,7 @@ func (rh *RepositoryHandler) getIndexYamlWithNS(namespace string) (*helm_repo.In
|
|||||||
|
|
||||||
// Merge the content of mergingIndexFile to the baseIndex
|
// Merge the content of mergingIndexFile to the baseIndex
|
||||||
// The chart url should be without --chart-url prefix
|
// The chart url should be without --chart-url prefix
|
||||||
func (rh *RepositoryHandler) mergeIndexFile(namespace string,
|
func (c *Controller) mergeIndexFile(namespace string,
|
||||||
baseIndex *helm_repo.IndexFile,
|
baseIndex *helm_repo.IndexFile,
|
||||||
mergingIndexFile *helm_repo.IndexFile) {
|
mergingIndexFile *helm_repo.IndexFile) {
|
||||||
// Append entries
|
// Append entries
|
||||||
@ -304,12 +208,9 @@ func (rh *RepositoryHandler) mergeIndexFile(namespace string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate empty index file
|
// Generate empty index file
|
||||||
func emptyIndexFile() []byte {
|
func emptyIndexFile() *helm_repo.IndexFile {
|
||||||
emptyIndexFile := &helm_repo.IndexFile{}
|
emptyIndexFile := &helm_repo.IndexFile{}
|
||||||
emptyIndexFile.Generated = time.Now()
|
emptyIndexFile.Generated = time.Now().Round(time.Second)
|
||||||
|
|
||||||
// Ignore the error
|
return emptyIndexFile
|
||||||
rawData, _ := json.Marshal(emptyIndexFile)
|
|
||||||
|
|
||||||
return rawData
|
|
||||||
}
|
}
|
32
src/chartserver/handler_repo_test.go
Normal file
32
src/chartserver/handler_repo_test.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package chartserver
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Test get /index.yaml
|
||||||
|
func TestGetIndexFile(t *testing.T) {
|
||||||
|
s, c, err := createMockObjects()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
namespaces := []string{"repo1", "repo2"}
|
||||||
|
indexFile, err := c.GetIndexFile(namespaces)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(indexFile.Entries) != 5 {
|
||||||
|
t.Fatalf("Expect index file with 5 entries, but got %d", len(indexFile.Entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := indexFile.Entries["repo1/harbor"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Expect chart entry 'repo1/harbor' but got nothing")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = indexFile.Entries["repo2/harbor"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Expect chart entry 'repo2/harbor' but got nothing")
|
||||||
|
}
|
||||||
|
}
|
208
src/chartserver/handler_utility.go
Normal file
208
src/chartserver/handler_utility.go
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package chartserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"k8s.io/helm/cmd/helm/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxDeletionThreads = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeleteChart deletes all the chart versions of the specified chart under the namespace.
|
||||||
|
// See @ServiceHandler.DeleteChart
|
||||||
|
func (c *Controller) DeleteChart(namespace, chartName string) error {
|
||||||
|
if len(strings.TrimSpace(namespace)) == 0 {
|
||||||
|
return errors.New("empty namespace when deleting chart")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(strings.TrimSpace(chartName)) == 0 {
|
||||||
|
return errors.New("empty chart name when deleting chart")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/%s", c.APIPrefix(namespace), chartName)
|
||||||
|
|
||||||
|
content, err := c.apiClient.GetContent(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allVersions, err := c.chartOperator.GetChartVersions(content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's delete the versions in parallel
|
||||||
|
// The number of goroutine is controlled by the const maxDeletionThreads
|
||||||
|
qSize := len(allVersions)
|
||||||
|
if qSize > maxDeletionThreads {
|
||||||
|
qSize = maxDeletionThreads
|
||||||
|
}
|
||||||
|
tokenQueue := make(chan struct{}, qSize)
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
waitGroup := new(sync.WaitGroup)
|
||||||
|
waitGroup.Add(len(allVersions))
|
||||||
|
|
||||||
|
// Append initial tokens
|
||||||
|
for i := 0; i < qSize; i++ {
|
||||||
|
tokenQueue <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect errors
|
||||||
|
errs := make([]error, 0)
|
||||||
|
errWrapper := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
// pass to the out func
|
||||||
|
if len(errs) > 0 {
|
||||||
|
errWrapper <- fmt.Errorf("%v", errs)
|
||||||
|
}
|
||||||
|
close(errWrapper)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for deletionErr := range errChan {
|
||||||
|
errs = append(errs, deletionErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Schedule deletion tasks
|
||||||
|
for _, deletingVersion := range allVersions {
|
||||||
|
// Apply for token first
|
||||||
|
// If no available token, pending here
|
||||||
|
<-tokenQueue
|
||||||
|
|
||||||
|
// Got one token
|
||||||
|
go func(deletingVersion *ChartVersion) {
|
||||||
|
defer func() {
|
||||||
|
// return the token back
|
||||||
|
tokenQueue <- struct{}{}
|
||||||
|
|
||||||
|
// done
|
||||||
|
waitGroup.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := c.DeleteChartVersion(namespace, chartName, deletingVersion.GetVersion()); err != nil {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
}(deletingVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait all goroutines are done
|
||||||
|
waitGroup.Wait()
|
||||||
|
// Safe to quit error collection goroutine
|
||||||
|
close(errChan)
|
||||||
|
|
||||||
|
err = <-errWrapper
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChartVersionDetails get the specified version for one chart
|
||||||
|
// This handler should return the details of the chart version,
|
||||||
|
// maybe including metadata,dependencies and values etc.
|
||||||
|
// See @ServiceHandler.GetChartVersionDetails
|
||||||
|
func (c *Controller) GetChartVersionDetails(namespace, chartName, version string) (*ChartVersionDetails, error) {
|
||||||
|
chartV, err := c.GetChartVersion(namespace, chartName, version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query cache
|
||||||
|
chartDetails := c.chartCache.GetChart(chartV.Digest)
|
||||||
|
if chartDetails == nil {
|
||||||
|
// NOT hit!!
|
||||||
|
content, err := c.getChartVersionContent(namespace, chartV.URLs[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process bytes and get more details of chart version
|
||||||
|
chartDetails, err = c.chartOperator.GetChartDetails(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
chartDetails.Metadata = chartV
|
||||||
|
|
||||||
|
// Put it into the cache for next access
|
||||||
|
c.chartCache.PutChart(chartDetails)
|
||||||
|
} else {
|
||||||
|
// Just logged
|
||||||
|
hlog.Debugf("Get detailed data from cache for chart: %s:%s (%s)",
|
||||||
|
chartDetails.Metadata.Name,
|
||||||
|
chartDetails.Metadata.Version,
|
||||||
|
chartDetails.Metadata.Digest)
|
||||||
|
}
|
||||||
|
// The change of prov file will not cause any influence to the digest of chart,
|
||||||
|
// and then the digital signature status should be not cached
|
||||||
|
//
|
||||||
|
// Generate the security report
|
||||||
|
// prov file share same endpoint with the chart version
|
||||||
|
// Just add .prov suffix to the chart version to form the path of prov file
|
||||||
|
// Anyway, there will be a report about the digital signature status
|
||||||
|
chartDetails.Security = &SecurityReport{
|
||||||
|
Signature: &DigitalSignature{
|
||||||
|
Signed: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// Try to get the prov file to confirm if it is exitsing
|
||||||
|
provFilePath := fmt.Sprintf("%s.prov", chartV.URLs[0])
|
||||||
|
provBytes, err := c.getChartVersionContent(namespace, provFilePath)
|
||||||
|
if err == nil && len(provBytes) > 0 {
|
||||||
|
chartDetails.Security.Signature.Signed = true
|
||||||
|
chartDetails.Security.Signature.Provenance = provFilePath
|
||||||
|
} else {
|
||||||
|
// Just log it
|
||||||
|
hlog.Errorf("Failed to get prov file for chart %s with error: %s, got %d bytes", chartV.Name, err.Error(), len(provBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
return chartDetails, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchChart search charts in the specified namespaces with the keyword q.
|
||||||
|
// RegExp mode is enabled as default.
|
||||||
|
// For each chart, only the latest version will shown in the result list if matched to avoid duplicated entries.
|
||||||
|
// Keep consistent with `helm search` command.
|
||||||
|
func (c *Controller) SearchChart(q string, namespaces []string) ([]*search.Result, error) {
|
||||||
|
if len(q) == 0 || len(namespaces) == 0 {
|
||||||
|
// Return empty list
|
||||||
|
return []*search.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the merged index yaml file of the namespaces
|
||||||
|
ind, err := c.getIndexYaml(namespaces)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the search index
|
||||||
|
index := search.NewIndex()
|
||||||
|
// As the repo name is already merged into the index yaml, we use empty repo name.
|
||||||
|
// Set 'All' to false to return only one version for each chart.
|
||||||
|
index.AddRepo("", ind, false)
|
||||||
|
|
||||||
|
// Search
|
||||||
|
// RegExp is enabled
|
||||||
|
results, err := index.Search(q, searchMaxScore, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort results.
|
||||||
|
search.SortScore(results)
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the content bytes of the chart version
|
||||||
|
func (c *Controller) getChartVersionContent(namespace string, subPath string) ([]byte, error) {
|
||||||
|
url := path.Join(namespace, subPath)
|
||||||
|
url = fmt.Sprintf("%s/%s", c.backendServerAddress.String(), url)
|
||||||
|
|
||||||
|
return c.apiClient.GetContent(url)
|
||||||
|
}
|
83
src/chartserver/handler_utility_test.go
Normal file
83
src/chartserver/handler_utility_test.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package chartserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test the function DeleteChart
|
||||||
|
func TestDeleteChart(t *testing.T) {
|
||||||
|
s, c, err := createMockObjects()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
if err := c.DeleteChart("repo1", "harbor"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test get /api/:repo/charts/:chart_name/:version
|
||||||
|
func TestGetChartVersion(t *testing.T) {
|
||||||
|
s, c, err := createMockObjects()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
chartVersion, err := c.GetChartVersionDetails("repo1", "harbor", "0.2.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if chartVersion.Metadata.Name != "harbor" {
|
||||||
|
t.Fatalf("Expect harbor chart version but got %s", chartVersion.Metadata.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if chartVersion.Metadata.Version != "0.2.0" {
|
||||||
|
t.Fatalf("Expect version '0.2.0' but got version %s", chartVersion.Metadata.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chartVersion.Dependencies) != 1 {
|
||||||
|
t.Fatalf("Expect 1 dependency but got %d ones", len(chartVersion.Dependencies))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chartVersion.Values) != 99 {
|
||||||
|
t.Fatalf("Expect 99 k-v values but got %d", len(chartVersion.Values))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test get /api/:repo/charts/:chart_name/:version with none-existing version
|
||||||
|
func TestGetChartVersionWithError(t *testing.T) {
|
||||||
|
s, c, err := createMockObjects()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
_, err = c.GetChartVersionDetails("repo1", "harbor", "1.0.0")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expect an error but got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the chart searching
|
||||||
|
func TestChartSearching(t *testing.T) {
|
||||||
|
s, c, err := createMockObjects()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
namespaces := []string{"repo1", "repo2"}
|
||||||
|
q := "harbor"
|
||||||
|
|
||||||
|
results, err := c.SearchChart(q, namespaces)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expect nil error but got '%s'", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Fatalf("expect 2 results but got %d", len(results))
|
||||||
|
}
|
||||||
|
}
|
@ -1,199 +0,0 @@
|
|||||||
package chartserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ghodss/yaml"
|
|
||||||
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
|
||||||
helm_repo "k8s.io/helm/pkg/repo"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// NamespaceContextKey is context key for the namespace
|
|
||||||
NamespaceContextKey ContextKey = ":repo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ContextKey is defined for add value in the context of http request
|
|
||||||
type ContextKey string
|
|
||||||
|
|
||||||
// ManipulationHandler includes all the handler methods for the purpose of manipulating the
|
|
||||||
// chart repository
|
|
||||||
type ManipulationHandler struct {
|
|
||||||
// Proxy used to to transfer the traffic of requests
|
|
||||||
// It's mainly used to talk to the backend chart server
|
|
||||||
trafficProxy *ProxyEngine
|
|
||||||
|
|
||||||
// Parse and process the chart version to provide required info data
|
|
||||||
chartOperator *ChartOperator
|
|
||||||
|
|
||||||
// HTTP client used to call the realted APIs of the backend chart repositories
|
|
||||||
apiClient *ChartClient
|
|
||||||
|
|
||||||
// Point to the url of the backend server
|
|
||||||
backendServerAddress *url.URL
|
|
||||||
|
|
||||||
// Cache the chart data
|
|
||||||
chartCache *ChartCache
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListCharts lists all the charts under the specified namespace
|
|
||||||
func (mh *ManipulationHandler) ListCharts(w http.ResponseWriter, req *http.Request) {
|
|
||||||
url := strings.TrimPrefix(req.URL.String(), "/")
|
|
||||||
url = fmt.Sprintf("%s/%s", mh.backendServerAddress.String(), url)
|
|
||||||
|
|
||||||
content, err := mh.apiClient.GetContent(url)
|
|
||||||
if err != nil {
|
|
||||||
WriteInternalError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
chartList, err := mh.chartOperator.GetChartList(content)
|
|
||||||
if err != nil {
|
|
||||||
WriteInternalError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonData, err := json.Marshal(chartList)
|
|
||||||
if err != nil {
|
|
||||||
WriteInternalError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writeJSONData(w, jsonData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChart returns all the chart versions under the specified chart
|
|
||||||
func (mh *ManipulationHandler) GetChart(w http.ResponseWriter, req *http.Request) {
|
|
||||||
mh.trafficProxy.ServeHTTP(w, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChartVersion get the specified version for one chart
|
|
||||||
// This handler should return the details of the chart version,
|
|
||||||
// maybe including metadata,dependencies and values etc.
|
|
||||||
func (mh *ManipulationHandler) GetChartVersion(w http.ResponseWriter, req *http.Request) {
|
|
||||||
chartV, err := mh.getChartVersion(req.URL.String())
|
|
||||||
if err != nil {
|
|
||||||
WriteInternalError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and check namespace
|
|
||||||
// even we get the data from cache
|
|
||||||
var namespace string
|
|
||||||
|
|
||||||
repoValue := req.Context().Value(NamespaceContextKey)
|
|
||||||
if repoValue != nil {
|
|
||||||
if ns, ok := repoValue.(string); ok {
|
|
||||||
namespace = ns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(strings.TrimSpace(namespace)) == 0 {
|
|
||||||
WriteInternalError(w, errors.New("failed to extract namespace from the request"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query cache
|
|
||||||
chartDetails := mh.chartCache.GetChart(chartV.Digest)
|
|
||||||
if chartDetails == nil {
|
|
||||||
// NOT hit!!
|
|
||||||
content, err := mh.getChartVersionContent(namespace, chartV.URLs[0])
|
|
||||||
if err != nil {
|
|
||||||
WriteInternalError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process bytes and get more details of chart version
|
|
||||||
chartDetails, err = mh.chartOperator.GetChartDetails(content)
|
|
||||||
if err != nil {
|
|
||||||
WriteInternalError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
chartDetails.Metadata = chartV
|
|
||||||
|
|
||||||
// Put it into the cache for next access
|
|
||||||
mh.chartCache.PutChart(chartDetails)
|
|
||||||
} else {
|
|
||||||
// Just logged
|
|
||||||
hlog.Debugf("Get detailed data from cache for chart: %s:%s (%s)",
|
|
||||||
chartDetails.Metadata.Name,
|
|
||||||
chartDetails.Metadata.Version,
|
|
||||||
chartDetails.Metadata.Digest)
|
|
||||||
}
|
|
||||||
// The change of prov file will not cause any influence to the digest of chart,
|
|
||||||
// and then the digital signature status should be not cached
|
|
||||||
//
|
|
||||||
// Generate the security report
|
|
||||||
// prov file share same endpoint with the chart version
|
|
||||||
// Just add .prov suffix to the chart version to form the path of prov file
|
|
||||||
// Anyway, there will be a report about the digital signature status
|
|
||||||
chartDetails.Security = &SecurityReport{
|
|
||||||
Signature: &DigitalSignature{
|
|
||||||
Signed: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Try to get the prov file to confirm if it is exitsing
|
|
||||||
provFilePath := fmt.Sprintf("%s.prov", chartV.URLs[0])
|
|
||||||
provBytes, err := mh.getChartVersionContent(namespace, provFilePath)
|
|
||||||
if err == nil && len(provBytes) > 0 {
|
|
||||||
chartDetails.Security.Signature.Signed = true
|
|
||||||
chartDetails.Security.Signature.Provenance = provFilePath
|
|
||||||
} else {
|
|
||||||
// Just log it
|
|
||||||
hlog.Errorf("Failed to get prov file for chart %s with error: %s, got %d bytes", chartV.Name, err.Error(), len(provBytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := json.Marshal(chartDetails)
|
|
||||||
if err != nil {
|
|
||||||
WriteInternalError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writeJSONData(w, bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadChartVersion will save the new version of the chart to the backend storage
|
|
||||||
func (mh *ManipulationHandler) UploadChartVersion(w http.ResponseWriter, req *http.Request) {
|
|
||||||
mh.trafficProxy.ServeHTTP(w, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadProvenanceFile will save the provenance file of the chart to the backend storage
|
|
||||||
func (mh *ManipulationHandler) UploadProvenanceFile(w http.ResponseWriter, req *http.Request) {
|
|
||||||
mh.trafficProxy.ServeHTTP(w, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteChartVersion will delete the specified version of the chart
|
|
||||||
func (mh *ManipulationHandler) DeleteChartVersion(w http.ResponseWriter, req *http.Request) {
|
|
||||||
mh.trafficProxy.ServeHTTP(w, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the basic metadata of chart version
|
|
||||||
func (mh *ManipulationHandler) getChartVersion(subPath string) (*helm_repo.ChartVersion, error) {
|
|
||||||
url := fmt.Sprintf("%s/%s", mh.backendServerAddress.String(), strings.TrimPrefix(subPath, "/"))
|
|
||||||
|
|
||||||
content, err := mh.apiClient.GetContent(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
chartVersion := &helm_repo.ChartVersion{}
|
|
||||||
if err := yaml.Unmarshal(content, chartVersion); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return chartVersion, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the content bytes of the chart version
|
|
||||||
func (mh *ManipulationHandler) getChartVersionContent(namespace string, subPath string) ([]byte, error) {
|
|
||||||
url := path.Join(namespace, subPath)
|
|
||||||
url = fmt.Sprintf("%s/%s", mh.backendServerAddress.String(), url)
|
|
||||||
|
|
||||||
return mh.apiClient.GetContent(url)
|
|
||||||
}
|
|
@ -17,6 +17,11 @@ import (
|
|||||||
const (
|
const (
|
||||||
agentHarbor = "HARBOR"
|
agentHarbor = "HARBOR"
|
||||||
contentLengthHeader = "Content-Length"
|
contentLengthHeader = "Content-Length"
|
||||||
|
|
||||||
|
defaultRepo = "library"
|
||||||
|
rootUploadingEndpoint = "/api/chartrepo/charts"
|
||||||
|
rootIndexEndpoint = "/chartrepo/index.yaml"
|
||||||
|
chartRepoHealthEndpoint = "/api/chartrepo/health"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProxyEngine is used to proxy the related traffics
|
// ProxyEngine is used to proxy the related traffics
|
||||||
@ -56,6 +61,7 @@ func director(target *url.URL, cred *Credential, req *http.Request) {
|
|||||||
// Overwrite the request URL to the target path
|
// Overwrite the request URL to the target path
|
||||||
req.URL.Scheme = target.Scheme
|
req.URL.Scheme = target.Scheme
|
||||||
req.URL.Host = target.Host
|
req.URL.Host = target.Host
|
||||||
|
rewriteURLPath(req)
|
||||||
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
||||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||||
@ -125,3 +131,34 @@ func singleJoiningSlash(a, b string) string {
|
|||||||
}
|
}
|
||||||
return a + b
|
return a + b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rewrite the incoming URL with the right backend URL pattern
|
||||||
|
// Remove 'chartrepo' from the endpoints of manipulation API
|
||||||
|
// Remove 'chartrepo' from the endpoints of repository services
|
||||||
|
func rewriteURLPath(req *http.Request) {
|
||||||
|
incomingURLPath := req.RequestURI
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
if incomingURLPath == chartRepoHealthEndpoint {
|
||||||
|
req.URL.Path = "/health"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root uploading endpoint
|
||||||
|
if incomingURLPath == rootUploadingEndpoint {
|
||||||
|
req.URL.Path = strings.Replace(incomingURLPath, "chartrepo", defaultRepo, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository endpoints
|
||||||
|
if strings.HasPrefix(incomingURLPath, "/chartrepo") {
|
||||||
|
req.URL.Path = strings.TrimPrefix(incomingURLPath, "/chartrepo")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// API endpoints
|
||||||
|
if strings.HasPrefix(incomingURLPath, "/api/chartrepo") {
|
||||||
|
req.URL.Path = strings.Replace(incomingURLPath, "/chartrepo", "", 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,163 +0,0 @@
|
|||||||
package chartserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ghodss/yaml"
|
|
||||||
helm_repo "k8s.io/helm/pkg/repo"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxDeletionThreads = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
// UtilityHandler provides utility methods
|
|
||||||
type UtilityHandler struct {
|
|
||||||
// Parse and process the chart version to provide required info data
|
|
||||||
chartOperator *ChartOperator
|
|
||||||
|
|
||||||
// HTTP client used to call the realted APIs of the backend chart repositories
|
|
||||||
apiClient *ChartClient
|
|
||||||
|
|
||||||
// Point to the url of the backend server
|
|
||||||
backendServerAddress *url.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChartsByNs gets the chart list under the namespace
|
|
||||||
func (uh *UtilityHandler) GetChartsByNs(namespace string) ([]*ChartInfo, error) {
|
|
||||||
if len(strings.TrimSpace(namespace)) == 0 {
|
|
||||||
return nil, errors.New("empty namespace when getting chart list")
|
|
||||||
}
|
|
||||||
|
|
||||||
path := fmt.Sprintf("/api/%s/charts", namespace)
|
|
||||||
url := fmt.Sprintf("%s%s", uh.backendServerAddress.String(), path)
|
|
||||||
|
|
||||||
content, err := uh.apiClient.GetContent(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return uh.chartOperator.GetChartList(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteChart deletes all the chart versions of the specified chart under the namespace.
|
|
||||||
func (uh *UtilityHandler) DeleteChart(namespace, chartName string) error {
|
|
||||||
if len(strings.TrimSpace(namespace)) == 0 {
|
|
||||||
return errors.New("empty namespace when deleting chart")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(strings.TrimSpace(chartName)) == 0 {
|
|
||||||
return errors.New("empty chart name when deleting chart")
|
|
||||||
}
|
|
||||||
|
|
||||||
path := fmt.Sprintf("/api/%s/charts/%s", namespace, chartName)
|
|
||||||
url := fmt.Sprintf("%s%s", uh.backendServerAddress.String(), path)
|
|
||||||
|
|
||||||
content, err := uh.apiClient.GetContent(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
allVersions, err := uh.chartOperator.GetChartVersions(content)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's delete the versions in parallel
|
|
||||||
// The number of goroutine is controlled by the const maxDeletionThreads
|
|
||||||
qSize := len(allVersions)
|
|
||||||
if qSize > maxDeletionThreads {
|
|
||||||
qSize = maxDeletionThreads
|
|
||||||
}
|
|
||||||
tokenQueue := make(chan struct{}, qSize)
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
waitGroup := new(sync.WaitGroup)
|
|
||||||
waitGroup.Add(len(allVersions))
|
|
||||||
|
|
||||||
// Append initial tokens
|
|
||||||
for i := 0; i < qSize; i++ {
|
|
||||||
tokenQueue <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect errors
|
|
||||||
errs := make([]error, 0)
|
|
||||||
errWrapper := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
// pass to the out func
|
|
||||||
if len(errs) > 0 {
|
|
||||||
errWrapper <- fmt.Errorf("%v", errs)
|
|
||||||
}
|
|
||||||
close(errWrapper)
|
|
||||||
}()
|
|
||||||
|
|
||||||
for deletionErr := range errChan {
|
|
||||||
errs = append(errs, deletionErr)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Schedule deletion tasks
|
|
||||||
for _, deletingVersion := range allVersions {
|
|
||||||
// Apply for token first
|
|
||||||
// If no available token, pending here
|
|
||||||
<-tokenQueue
|
|
||||||
|
|
||||||
// Got one token
|
|
||||||
go func(deletingVersion *helm_repo.ChartVersion) {
|
|
||||||
defer func() {
|
|
||||||
// return the token back
|
|
||||||
tokenQueue <- struct{}{}
|
|
||||||
|
|
||||||
// done
|
|
||||||
waitGroup.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := uh.deleteChartVersion(namespace, chartName, deletingVersion.GetVersion()); err != nil {
|
|
||||||
errChan <- err
|
|
||||||
}
|
|
||||||
}(deletingVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait all goroutines are done
|
|
||||||
waitGroup.Wait()
|
|
||||||
// Safe to quit error collection goroutine
|
|
||||||
close(errChan)
|
|
||||||
|
|
||||||
err = <-errWrapper
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChartVersion returns the summary of the specified chart version.
|
|
||||||
func (uh *UtilityHandler) GetChartVersion(namespace, name, version string) (*helm_repo.ChartVersion, error) {
|
|
||||||
if len(namespace) == 0 || len(name) == 0 || len(version) == 0 {
|
|
||||||
return nil, errors.New("bad arguments to get chart version summary")
|
|
||||||
}
|
|
||||||
|
|
||||||
path := fmt.Sprintf("/api/%s/charts/%s/%s", namespace, name, version)
|
|
||||||
url := fmt.Sprintf("%s%s", uh.backendServerAddress.String(), path)
|
|
||||||
|
|
||||||
content, err := uh.apiClient.GetContent(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
chartVersion := &helm_repo.ChartVersion{}
|
|
||||||
if err := yaml.Unmarshal(content, chartVersion); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return chartVersion, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteChartVersion deletes the specified chart version
|
|
||||||
func (uh *UtilityHandler) deleteChartVersion(namespace, chartName, version string) error {
|
|
||||||
path := fmt.Sprintf("/api/%s/charts/%s/%s", namespace, chartName, version)
|
|
||||||
url := fmt.Sprintf("%s%s", uh.backendServerAddress.String(), path)
|
|
||||||
|
|
||||||
return uh.apiClient.DeleteContent(url)
|
|
||||||
}
|
|
@ -1,228 +0,0 @@
|
|||||||
package chartserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestGetChartsByNs tests GetChartsByNs method in UtilityHandler
|
|
||||||
func TestGetChartsByNs(t *testing.T) {
|
|
||||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.RequestURI {
|
|
||||||
case "/api/repo1/charts":
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
w.Write(chartListContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
|
||||||
w.Write([]byte("not supported"))
|
|
||||||
}))
|
|
||||||
defer mockServer.Close()
|
|
||||||
|
|
||||||
serverURL, err := url.Parse(mockServer.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
theController, err := NewController(serverURL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
charts, err := theController.GetUtilityHandler().GetChartsByNs("repo1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(charts) != 2 {
|
|
||||||
t.Fatalf("expect 2 items but got %d", len(charts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the function DeleteChart
|
|
||||||
func TestDeleteChart(t *testing.T) {
|
|
||||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.RequestURI {
|
|
||||||
case "/api/repo1/charts/harbor":
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
w.Write([]byte(chartVersionsOfHarbor))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case "/api/repo1/charts/harbor/0.2.0",
|
|
||||||
"/api/repo1/charts/harbor/0.2.1":
|
|
||||||
if r.Method == http.MethodDelete {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
|
||||||
w.Write([]byte("not supported"))
|
|
||||||
}))
|
|
||||||
defer mockServer.Close()
|
|
||||||
|
|
||||||
serverURL, err := url.Parse(mockServer.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
theController, err := NewController(serverURL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := theController.GetUtilityHandler().DeleteChart("repo1", "harbor"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the GetChartVersion in utility handler
|
|
||||||
func TestGetChartVersionSummary(t *testing.T) {
|
|
||||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.RequestURI {
|
|
||||||
case "/api/repo1/charts/harbor/0.2.0":
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
w.Write([]byte(chartVersionOfHarbor020))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
|
||||||
w.Write([]byte("not supported"))
|
|
||||||
}))
|
|
||||||
defer mockServer.Close()
|
|
||||||
|
|
||||||
serverURL, err := url.Parse(mockServer.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
theController, err := NewController(serverURL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
chartV, err := theController.GetUtilityHandler().GetChartVersion("repo1", "harbor", "0.2.0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if chartV.GetName() != "harbor" {
|
|
||||||
t.Fatalf("expect chart name 'harbor' but got '%s'", chartV.GetName())
|
|
||||||
}
|
|
||||||
|
|
||||||
if chartV.GetVersion() != "0.2.0" {
|
|
||||||
t.Fatalf("expect chart version '0.2.0' but got '%s'", chartV.GetVersion())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var chartVersionOfHarbor020 = `
|
|
||||||
{
|
|
||||||
"name": "harbor",
|
|
||||||
"home": "https://github.com/vmware/harbor",
|
|
||||||
"sources": [
|
|
||||||
"https://github.com/vmware/harbor/tree/master/contrib/helm/harbor"
|
|
||||||
],
|
|
||||||
"version": "0.2.0",
|
|
||||||
"description": "An Enterprise-class Docker Registry by VMware",
|
|
||||||
"keywords": [
|
|
||||||
"vmware",
|
|
||||||
"docker",
|
|
||||||
"registry",
|
|
||||||
"harbor"
|
|
||||||
],
|
|
||||||
"maintainers": [
|
|
||||||
{
|
|
||||||
"name": "Jesse Hu",
|
|
||||||
"email": "huh@vmware.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "paulczar",
|
|
||||||
"email": "username.taken@gmail.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"engine": "gotpl",
|
|
||||||
"icon": "https://raw.githubusercontent.com/vmware/harbor/master/docs/img/harbor_logo.png",
|
|
||||||
"appVersion": "1.5.0",
|
|
||||||
"urls": [
|
|
||||||
"charts/harbor-0.2.0.tgz"
|
|
||||||
],
|
|
||||||
"created": "2018-08-29T10:26:21.141611102Z",
|
|
||||||
"digest": "fc8aae8dade9f0dfca12e9f1085081c49843d30a063a3fa7eb42497e3ceb277c"
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
var chartVersionsOfHarbor = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "harbor",
|
|
||||||
"home": "https://github.com/vmware/harbor",
|
|
||||||
"sources": [
|
|
||||||
"https://github.com/vmware/harbor/tree/master/contrib/helm/harbor"
|
|
||||||
],
|
|
||||||
"version": "0.2.1",
|
|
||||||
"description": "An Enterprise-class Docker Registry by VMware",
|
|
||||||
"keywords": [
|
|
||||||
"vmware",
|
|
||||||
"docker",
|
|
||||||
"registry",
|
|
||||||
"harbor"
|
|
||||||
],
|
|
||||||
"maintainers": [
|
|
||||||
{
|
|
||||||
"name": "Jesse Hu",
|
|
||||||
"email": "huh@vmware.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "paulczar",
|
|
||||||
"email": "username.taken@gmail.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"engine": "gotpl",
|
|
||||||
"icon": "https://raw.githubusercontent.com/vmware/harbor/master/docs/img/harbor_logo.png",
|
|
||||||
"appVersion": "1.5.0",
|
|
||||||
"urls": [
|
|
||||||
"charts/harbor-0.2.1.tgz"
|
|
||||||
],
|
|
||||||
"created": "2018-08-29T10:26:29.625749155Z",
|
|
||||||
"digest": "2538edf4ddb797af8e025f3bd6226270440110bbdb689bad48656a519a154236"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "harbor",
|
|
||||||
"home": "https://github.com/vmware/harbor",
|
|
||||||
"sources": [
|
|
||||||
"https://github.com/vmware/harbor/tree/master/contrib/helm/harbor"
|
|
||||||
],
|
|
||||||
"version": "0.2.0",
|
|
||||||
"description": "An Enterprise-class Docker Registry by VMware",
|
|
||||||
"keywords": [
|
|
||||||
"vmware",
|
|
||||||
"docker",
|
|
||||||
"registry",
|
|
||||||
"harbor"
|
|
||||||
],
|
|
||||||
"maintainers": [
|
|
||||||
{
|
|
||||||
"name": "Jesse Hu",
|
|
||||||
"email": "huh@vmware.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "paulczar",
|
|
||||||
"email": "username.taken@gmail.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"engine": "gotpl",
|
|
||||||
"icon": "https://raw.githubusercontent.com/vmware/harbor/master/docs/img/harbor_logo.png",
|
|
||||||
"appVersion": "1.5.0",
|
|
||||||
"urls": [
|
|
||||||
"charts/harbor-0.2.0.tgz"
|
|
||||||
],
|
|
||||||
"created": "2018-08-29T10:26:21.141611102Z",
|
|
||||||
"digest": "fc8aae8dade9f0dfca12e9f1085081c49843d30a063a3fa7eb42497e3ceb277c"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
`
|
|
@ -4,8 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -14,30 +14,6 @@ const (
|
|||||||
contentTypeJSON = "application/json"
|
contentTypeJSON = "application/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriteError writes error to http client
|
|
||||||
func WriteError(w http.ResponseWriter, code int, err error) {
|
|
||||||
errorObj := make(map[string]string)
|
|
||||||
errorObj["error"] = err.Error()
|
|
||||||
errorContent, errorMarshal := json.Marshal(errorObj)
|
|
||||||
if errorMarshal != nil {
|
|
||||||
errorContent = []byte(err.Error())
|
|
||||||
}
|
|
||||||
w.WriteHeader(code)
|
|
||||||
w.Write(errorContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteInternalError writes error with statusCode == 500
|
|
||||||
func WriteInternalError(w http.ResponseWriter, err error) {
|
|
||||||
WriteError(w, http.StatusInternalServerError, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write JSON data to http client
|
|
||||||
func writeJSONData(w http.ResponseWriter, data []byte) {
|
|
||||||
w.Header().Set(contentTypeHeader, contentTypeJSON)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract error object '{"error": "****---***"}' from the content if existing
|
// Extract error object '{"error": "****---***"}' from the content if existing
|
||||||
// nil error will be returned if it does exist
|
// nil error will be returned if it does exist
|
||||||
func extractError(content []byte) error {
|
func extractError(content []byte) error {
|
||||||
@ -116,3 +92,38 @@ func parseRedisConfig(redisConfigV string) (string, error) {
|
|||||||
|
|
||||||
return string(cfgData), nil
|
return string(cfgData), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What's the cache driver if it is set
|
||||||
|
func parseCacheDriver() (string, bool) {
|
||||||
|
driver, ok := os.LookupEnv(cacheDriverENVKey)
|
||||||
|
return strings.ToLower(driver), ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and parse the configuration for the chart cache
|
||||||
|
func getCacheConfig() (*ChartCacheConfig, error) {
|
||||||
|
driver, isSet := parseCacheDriver()
|
||||||
|
if !isSet {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if driver != cacheDriverMem && driver != cacheDriverRedis {
|
||||||
|
return nil, fmt.Errorf("cache driver '%s' is not supported, only support 'memory' and 'redis'", driver)
|
||||||
|
}
|
||||||
|
|
||||||
|
if driver == cacheDriverMem {
|
||||||
|
return &ChartCacheConfig{
|
||||||
|
DriverType: driver,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
redisConfigV := os.Getenv(redisENVKey)
|
||||||
|
redisCfg, err := parseRedisConfig(redisConfigV)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse redis configurations from '%s' with error: %s", redisCfg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ChartCacheConfig{
|
||||||
|
DriverType: driver,
|
||||||
|
Config: redisCfg,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
@ -17,6 +17,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
yaml "github.com/ghodss/yaml"
|
||||||
"github.com/goharbor/harbor/src/common/api"
|
"github.com/goharbor/harbor/src/common/api"
|
||||||
"github.com/goharbor/harbor/src/common/security"
|
"github.com/goharbor/harbor/src/common/security"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
@ -26,6 +27,10 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/ui/utils"
|
"github.com/goharbor/harbor/src/ui/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
yamlFileContentType = "application/x-yaml"
|
||||||
|
)
|
||||||
|
|
||||||
// BaseController ...
|
// BaseController ...
|
||||||
type BaseController struct {
|
type BaseController struct {
|
||||||
api.BaseAPI
|
api.BaseAPI
|
||||||
@ -98,6 +103,26 @@ func (b *BaseController) SendForbiddenError(err error) {
|
|||||||
b.RenderFormatedError(http.StatusForbidden, err)
|
b.RenderFormatedError(http.StatusForbidden, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteJSONData writes the JSON data to the client.
|
||||||
|
func (b *BaseController) WriteJSONData(object interface{}) {
|
||||||
|
b.Data["json"] = object
|
||||||
|
b.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteYamlData writes the yaml data to the client.
|
||||||
|
func (b *BaseController) WriteYamlData(object interface{}) {
|
||||||
|
yData, err := yaml.Marshal(object)
|
||||||
|
if err != nil {
|
||||||
|
b.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w := b.Ctx.ResponseWriter
|
||||||
|
w.Header().Set("Content-Type", yamlFileContentType)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(yData)
|
||||||
|
}
|
||||||
|
|
||||||
// Init related objects/configurations for the API controllers
|
// Init related objects/configurations for the API controllers
|
||||||
func Init() error {
|
func Init() error {
|
||||||
// If chart repository is not enabled then directly return
|
// If chart repository is not enabled then directly return
|
||||||
|
@ -55,7 +55,7 @@ func (cla *ChartLabelAPI) Prepare() {
|
|||||||
chartName := cla.GetStringFromPath(nameParam)
|
chartName := cla.GetStringFromPath(nameParam)
|
||||||
version := cla.GetStringFromPath(versionParam)
|
version := cla.GetStringFromPath(versionParam)
|
||||||
|
|
||||||
if _, err = chartController.GetUtilityHandler().GetChartVersion(project, chartName, version); err != nil {
|
if _, err = chartController.GetChartVersion(project, chartName, version); err != nil {
|
||||||
cla.SendNotFoundError(err)
|
cla.SendNotFoundError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -12,6 +11,9 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common"
|
||||||
|
"github.com/goharbor/harbor/src/ui/label"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/chartserver"
|
"github.com/goharbor/harbor/src/chartserver"
|
||||||
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/ui/config"
|
"github.com/goharbor/harbor/src/ui/config"
|
||||||
@ -45,6 +47,9 @@ type ChartRepositoryAPI struct {
|
|||||||
// The base controller to provide common utilities
|
// The base controller to provide common utilities
|
||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
|
// For label management
|
||||||
|
labelManager *label.BaseManager
|
||||||
|
|
||||||
// Keep the namespace if existing
|
// Keep the namespace if existing
|
||||||
namespace string
|
namespace string
|
||||||
}
|
}
|
||||||
@ -75,18 +80,19 @@ func (cra *ChartRepositoryAPI) Prepare() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite URL path
|
// Init label manager
|
||||||
cra.rewriteURLPath(cra.Ctx.Request)
|
cra.labelManager = &label.BaseManager{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHealthStatus handles GET /api/chartserver/health
|
// GetHealthStatus handles GET /api/chartrepo/health
|
||||||
func (cra *ChartRepositoryAPI) GetHealthStatus() {
|
func (cra *ChartRepositoryAPI) GetHealthStatus() {
|
||||||
// Check access
|
// Check access
|
||||||
if !cra.requireAccess(cra.namespace, accessLevelSystem) {
|
if !cra.requireAccess(cra.namespace, accessLevelSystem) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chartController.GetBaseHandler().GetHealthStatus(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
// Directly proxy to the backend
|
||||||
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIndexByRepo handles GET /:repo/index.yaml
|
// GetIndexByRepo handles GET /:repo/index.yaml
|
||||||
@ -96,7 +102,8 @@ func (cra *ChartRepositoryAPI) GetIndexByRepo() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chartController.GetRepositoryHandler().GetIndexFileWithNS(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
// Directly proxy to the backend
|
||||||
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIndex handles GET /index.yaml
|
// GetIndex handles GET /index.yaml
|
||||||
@ -106,7 +113,24 @@ func (cra *ChartRepositoryAPI) GetIndex() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chartController.GetRepositoryHandler().GetIndexFile(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
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
|
// DownloadChart handles GET /:repo/charts/:filename
|
||||||
@ -116,7 +140,8 @@ func (cra *ChartRepositoryAPI) DownloadChart() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chartController.GetRepositoryHandler().DownloadChartObject(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
// Directly proxy to the backend
|
||||||
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListCharts handles GET /api/:repo/charts
|
// ListCharts handles GET /api/:repo/charts
|
||||||
@ -126,7 +151,13 @@ func (cra *ChartRepositoryAPI) ListCharts() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chartController.GetManipulationHandler().ListCharts(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
charts, err := chartController.ListCharts(cra.namespace)
|
||||||
|
if err != nil {
|
||||||
|
cra.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cra.WriteJSONData(charts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListChartVersions GET /api/:repo/charts/:name
|
// ListChartVersions GET /api/:repo/charts/:name
|
||||||
@ -136,7 +167,25 @@ func (cra *ChartRepositoryAPI) ListChartVersions() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chartController.GetManipulationHandler().GetChart(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
chartName := cra.GetStringFromPath(nameParam)
|
||||||
|
|
||||||
|
versions, err := chartController.GetChart(cra.namespace, chartName)
|
||||||
|
if err != nil {
|
||||||
|
cra.SendInternalServerError(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
|
// GetChartVersion handles GET /api/:repo/charts/:name/:version
|
||||||
@ -146,11 +195,25 @@ func (cra *ChartRepositoryAPI) GetChartVersion() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's pass the namespace via the context of request
|
// Get other parameters
|
||||||
req := cra.Ctx.Request
|
chartName := cra.GetStringFromPath(nameParam)
|
||||||
*req = *(req.WithContext(context.WithValue(req.Context(), chartserver.NamespaceContextKey, cra.namespace)))
|
version := cra.GetStringFromPath(versionParam)
|
||||||
|
|
||||||
chartController.GetManipulationHandler().GetChartVersion(cra.Ctx.ResponseWriter, req)
|
chartVersion, err := chartController.GetChartVersionDetails(cra.namespace, chartName, version)
|
||||||
|
if err != nil {
|
||||||
|
cra.SendInternalServerError(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
|
// DeleteChartVersion handles DELETE /api/:repo/charts/:name/:version
|
||||||
@ -160,7 +223,20 @@ func (cra *ChartRepositoryAPI) DeleteChartVersion() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
chartController.GetManipulationHandler().DeleteChartVersion(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
// Get other parameters
|
||||||
|
chartName := cra.GetStringFromPath(nameParam)
|
||||||
|
version := cra.GetStringFromPath(versionParam)
|
||||||
|
|
||||||
|
// Try to remove labels from deleting chart if exitsing
|
||||||
|
if err := cra.removeLabelsFromChart(chartName, version); err != nil {
|
||||||
|
cra.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := chartController.DeleteChartVersion(cra.namespace, chartName, version); err != nil {
|
||||||
|
cra.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadChartVersion handles POST /api/:repo/charts
|
// UploadChartVersion handles POST /api/:repo/charts
|
||||||
@ -184,12 +260,13 @@ func (cra *ChartRepositoryAPI) UploadChartVersion() {
|
|||||||
formField: formFiledNameForProv,
|
formField: formFiledNameForProv,
|
||||||
})
|
})
|
||||||
if err := cra.rewriteFileContent(formFiles, cra.Ctx.Request); err != nil {
|
if err := cra.rewriteFileContent(formFiles, cra.Ctx.Request); err != nil {
|
||||||
chartserver.WriteInternalError(cra.Ctx.ResponseWriter, err)
|
cra.SendInternalServerError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chartController.GetManipulationHandler().UploadChartVersion(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
// Directly proxy to the backend
|
||||||
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadChartProvFile handles POST /api/:repo/prov
|
// UploadChartProvFile handles POST /api/:repo/prov
|
||||||
@ -208,12 +285,13 @@ func (cra *ChartRepositoryAPI) UploadChartProvFile() {
|
|||||||
mustHave: true,
|
mustHave: true,
|
||||||
})
|
})
|
||||||
if err := cra.rewriteFileContent(formFiles, cra.Ctx.Request); err != nil {
|
if err := cra.rewriteFileContent(formFiles, cra.Ctx.Request); err != nil {
|
||||||
chartserver.WriteInternalError(cra.Ctx.ResponseWriter, err)
|
cra.SendInternalServerError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chartController.GetManipulationHandler().UploadProvenanceFile(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
// Directly proxy to the backend
|
||||||
|
chartController.ProxyTraffic(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteChart deletes all the chart versions of the specified chart.
|
// DeleteChart deletes all the chart versions of the specified chart.
|
||||||
@ -226,44 +304,39 @@ func (cra *ChartRepositoryAPI) DeleteChart() {
|
|||||||
// Get other parameters from the request
|
// Get other parameters from the request
|
||||||
chartName := cra.GetStringFromPath(nameParam)
|
chartName := cra.GetStringFromPath(nameParam)
|
||||||
|
|
||||||
if err := chartController.GetUtilityHandler().DeleteChart(cra.namespace, chartName); err != nil {
|
// Remove labels from all the deleting chart versions under the chart
|
||||||
chartserver.WriteInternalError(cra.Ctx.ResponseWriter, err)
|
chartVersions, err := chartController.GetChart(cra.namespace, chartName)
|
||||||
|
if err != nil {
|
||||||
|
cra.SendInternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chartVersion := range chartVersions {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite the incoming URL with the right backend URL pattern
|
func (cra *ChartRepositoryAPI) removeLabelsFromChart(chartName, version string) error {
|
||||||
// Remove 'chartrepo' from the endpoints of manipulation API
|
// Try to remove labels from deleting chart if exitsing
|
||||||
// Remove 'chartrepo' from the endpoints of repository services
|
resourceID := chartFullName(cra.namespace, chartName, version)
|
||||||
func (cra *ChartRepositoryAPI) rewriteURLPath(req *http.Request) {
|
labels, err := cra.labelManager.GetLabelsOfResource(common.ResourceTypeChart, resourceID)
|
||||||
incomingURLPath := req.RequestURI
|
if err == nil && len(labels) > 0 {
|
||||||
|
for _, l := range labels {
|
||||||
defer func() {
|
if err := cra.labelManager.RemoveLabelFromResource(common.ResourceTypeChart, resourceID, l.ID); err != nil {
|
||||||
hlog.Debugf("Incoming URL '%s' is rewritten to '%s'", incomingURLPath, req.URL.String())
|
return err
|
||||||
}()
|
}
|
||||||
|
}
|
||||||
// Health check endpoint
|
|
||||||
if incomingURLPath == chartRepoHealthEndpoint {
|
|
||||||
req.URL.Path = "/health"
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root uploading endpoint
|
return nil
|
||||||
if incomingURLPath == rootUploadingEndpoint {
|
|
||||||
req.URL.Path = strings.Replace(incomingURLPath, "chartrepo", defaultRepo, 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repository endpoints
|
|
||||||
if strings.HasPrefix(incomingURLPath, "/chartrepo") {
|
|
||||||
req.URL.Path = strings.TrimPrefix(incomingURLPath, "/chartrepo")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// API endpoints
|
|
||||||
if strings.HasPrefix(incomingURLPath, "/api/chartrepo") {
|
|
||||||
req.URL.Path = strings.Replace(incomingURLPath, "/chartrepo", "", 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there exists a valid namespace
|
// Check if there exists a valid namespace
|
||||||
@ -272,20 +345,20 @@ func (cra *ChartRepositoryAPI) rewriteURLPath(req *http.Request) {
|
|||||||
func (cra *ChartRepositoryAPI) requireNamespace(namespace string) bool {
|
func (cra *ChartRepositoryAPI) requireNamespace(namespace string) bool {
|
||||||
// Actually, never should be like this
|
// Actually, never should be like this
|
||||||
if len(namespace) == 0 {
|
if len(namespace) == 0 {
|
||||||
cra.HandleBadRequest(":repo should be in the request URL")
|
cra.SendBadRequestError(errors.New(":repo should be in the request URL"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
existsing, err := cra.ProjectMgr.Exists(namespace)
|
existsing, err := cra.ProjectMgr.Exists(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Check failed with error
|
// Check failed with error
|
||||||
cra.renderError(http.StatusInternalServerError, fmt.Sprintf("failed to check existence of namespace %s with error: %s", namespace, err.Error()))
|
cra.SendInternalServerError(fmt.Errorf("failed to check existence of namespace %s with error: %s", namespace, err.Error()))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not existing
|
// Not existing
|
||||||
if !existsing {
|
if !existsing {
|
||||||
cra.renderError(http.StatusBadRequest, fmt.Sprintf("namespace %s is not existing", namespace))
|
cra.SendBadRequestError(fmt.Errorf("namespace %s is not existing", namespace))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,7 +401,7 @@ func (cra *ChartRepositoryAPI) requireAccess(namespace string, accessLevel uint)
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// access rejected for invalid scope
|
// access rejected for invalid scope
|
||||||
cra.renderError(http.StatusForbidden, "unrecognized access scope")
|
cra.SendForbiddenError(errors.New("unrecognized access scope"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,23 +409,18 @@ func (cra *ChartRepositoryAPI) requireAccess(namespace string, accessLevel uint)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// Unauthenticated, return 401
|
// Unauthenticated, return 401
|
||||||
if !cra.SecurityCtx.IsAuthenticated() {
|
if !cra.SecurityCtx.IsAuthenticated() {
|
||||||
cra.renderError(http.StatusUnauthorized, "Unauthorized")
|
cra.SendUnAuthorizedError(errors.New("Unauthorized"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticated, return 403
|
// Authenticated, return 403
|
||||||
cra.renderError(http.StatusForbidden, err.Error())
|
cra.SendForbiddenError(err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// write error message with unified format
|
|
||||||
func (cra *ChartRepositoryAPI) renderError(code int, text string) {
|
|
||||||
chartserver.WriteError(cra.Ctx.ResponseWriter, code, errors.New(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
// formFile is used to represent the uploaded files in the form
|
// formFile is used to represent the uploaded files in the form
|
||||||
type formFile struct {
|
type formFile struct {
|
||||||
// form field key contains the form file
|
// form field key contains the form file
|
||||||
@ -442,3 +510,8 @@ func initializeChartController() (*chartserver.Controller, error) {
|
|||||||
func isMultipartFormData(req *http.Request) bool {
|
func isMultipartFormData(req *http.Request) bool {
|
||||||
return strings.Contains(req.Header.Get(headerContentType), contentTypeMultipart)
|
return strings.Contains(req.Header.Get(headerContentType), contentTypeMultipart)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the chart full name
|
||||||
|
func chartFullName(namespace, chartName, version string) string {
|
||||||
|
return fmt.Sprintf("%s/%s:%s", namespace, chartName, version)
|
||||||
|
}
|
||||||
|
@ -9,46 +9,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/ui/promgr/metamgr"
|
"github.com/goharbor/harbor/src/ui/promgr/metamgr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test the URL rewrite function
|
|
||||||
func TestURLRewrite(t *testing.T) {
|
|
||||||
chartAPI := &ChartRepositoryAPI{}
|
|
||||||
req, err := createRequest(http.MethodGet, "/api/chartrepo/health")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
chartAPI.rewriteURLPath(req)
|
|
||||||
if req.URL.Path != "/health" {
|
|
||||||
t.Fatalf("Expect url format %s but got %s", "/health", req.URL.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err = createRequest(http.MethodGet, "/api/chartrepo/library/charts")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
chartAPI.rewriteURLPath(req)
|
|
||||||
if req.URL.Path != "/api/library/charts" {
|
|
||||||
t.Fatalf("Expect url format %s but got %s", "/api/library/charts", req.URL.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err = createRequest(http.MethodPost, "/api/chartrepo/charts")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
chartAPI.rewriteURLPath(req)
|
|
||||||
if req.URL.Path != "/api/library/charts" {
|
|
||||||
t.Fatalf("Expect url format %s but got %s", "/api/library/charts", req.URL.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err = createRequest(http.MethodGet, "/chartrepo/library/index.yaml")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
chartAPI.rewriteURLPath(req)
|
|
||||||
if req.URL.Path != "/library/index.yaml" {
|
|
||||||
t.Fatalf("Expect url format %s but got %s", "/library/index.yaml", req.URL.Path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test access checking
|
// Test access checking
|
||||||
func TestRequireAccess(t *testing.T) {
|
func TestRequireAccess(t *testing.T) {
|
||||||
chartAPI := &ChartRepositoryAPI{}
|
chartAPI := &ChartRepositoryAPI{}
|
||||||
|
@ -298,7 +298,7 @@ func (p *ProjectAPI) deletable(projectID int64) (*deletableResp, error) {
|
|||||||
|
|
||||||
// Check helm charts number
|
// Check helm charts number
|
||||||
if config.WithChartMuseum() {
|
if config.WithChartMuseum() {
|
||||||
charts, err := chartController.GetUtilityHandler().GetChartsByNs(p.project.Name)
|
charts, err := chartController.ListCharts(p.project.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ func (s *SearchAPI) Get() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if searchHandler == nil {
|
if searchHandler == nil {
|
||||||
searchHandler = chartController.GetRepositoryHandler().SearchChart
|
searchHandler = chartController.SearchChart
|
||||||
}
|
}
|
||||||
|
|
||||||
chartResults, err := searchHandler(keyword, proNames)
|
chartResults, err := searchHandler(keyword, proNames)
|
||||||
|
Loading…
Reference in New Issue
Block a user