Merge pull request #5524 from steven-zou/pro_deletable_enable

Enhance the project deletable checking logic to include helm charts checking
This commit is contained in:
Steven Zou 2018-08-06 16:00:38 +08:00 committed by GitHub
commit bc6d7b69e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 158 additions and 5 deletions

View File

@ -36,6 +36,9 @@ type Controller struct {
//To cover all the manipulation requests //To cover all the manipulation requests
manipulationHandler *ManipulationHandler manipulationHandler *ManipulationHandler
//To cover the other utility requests
utilityHandler *UtilityHandler
} }
//NewController is constructor of the chartserver.Controller //NewController is constructor of the chartserver.Controller
@ -86,6 +89,11 @@ func NewController(backendServer *url.URL) (*Controller, error) {
backendServerAddress: backendServer, backendServerAddress: backendServer,
chartCache: cache, chartCache: cache,
}, },
utilityHandler: &UtilityHandler{
apiClient: client,
backendServerAddress: backendServer,
chartOperator: operator,
},
}, nil }, nil
} }
@ -104,6 +112,11 @@ func (c *Controller) GetManipulationHandler() *ManipulationHandler {
return c.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 //What's the cache driver if it is set
func parseCacheDriver() (string, bool) { func parseCacheDriver() (string, bool) {
driver, ok := os.LookupEnv(cacheDriverENVKey) driver, ok := os.LookupEnv(cacheDriverENVKey)

View File

@ -0,0 +1,37 @@
package chartserver
import (
"errors"
"fmt"
"net/url"
"strings"
)
//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)
}

View File

@ -0,0 +1,44 @@
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))
}
}

View File

@ -217,7 +217,7 @@ func (p *ProjectAPI) Delete() {
return return
} }
result, err := deletable(p.project.ProjectID) result, err := p.deletable(p.project.ProjectID)
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf( p.HandleInternalServerError(fmt.Sprintf(
"failed to check the deletable of project %d: %v", p.project.ProjectID, err)) "failed to check the deletable of project %d: %v", p.project.ProjectID, err))
@ -258,7 +258,7 @@ func (p *ProjectAPI) Deletable() {
return return
} }
result, err := deletable(p.project.ProjectID) result, err := p.deletable(p.project.ProjectID)
if err != nil { if err != nil {
p.HandleInternalServerError(fmt.Sprintf( p.HandleInternalServerError(fmt.Sprintf(
"failed to check the deletable of project %d: %v", p.project.ProjectID, err)) "failed to check the deletable of project %d: %v", p.project.ProjectID, err))
@ -269,7 +269,7 @@ func (p *ProjectAPI) Deletable() {
p.ServeJSON() p.ServeJSON()
} }
func deletable(projectID int64) (*deletableResp, error) { func (p *ProjectAPI) deletable(projectID int64) (*deletableResp, error) {
count, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{ count, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{projectID}, ProjectIDs: []int64{projectID},
}) })
@ -280,7 +280,7 @@ func deletable(projectID int64) (*deletableResp, error) {
if count > 0 { if count > 0 {
return &deletableResp{ return &deletableResp{
Deletable: false, Deletable: false,
Message: "the project contains repositories, can not be deleled", Message: "the project contains repositories, can not be deleted",
}, nil }, nil
} }
@ -292,10 +292,25 @@ func deletable(projectID int64) (*deletableResp, error) {
if len(policies) > 0 { if len(policies) > 0 {
return &deletableResp{ return &deletableResp{
Deletable: false, Deletable: false,
Message: "the project contains replication rules, can not be deleled", Message: "the project contains replication rules, can not be deleted",
}, nil }, nil
} }
//Check helm charts number
if config.WithChartMuseum() {
charts, err := chartController.GetUtilityHandler().GetChartsByNs(p.project.Name)
if err != nil {
return nil, err
}
if len(charts) > 0 {
return &deletableResp{
Deletable: false,
Message: "the project contains helm charts, can not be deleted",
}, nil
}
}
return &deletableResp{ return &deletableResp{
Deletable: true, Deletable: true,
}, nil }, nil

View File

@ -16,10 +16,14 @@ package api
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest"
"net/url"
"strconv" "strconv"
"testing" "testing"
"time" "time"
"github.com/vmware/harbor/src/chartserver"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
@ -362,6 +366,13 @@ func TestProjectLogsFilter(t *testing.T) {
func TestDeletable(t *testing.T) { func TestDeletable(t *testing.T) {
apiTest := newHarborAPI() apiTest := newHarborAPI()
chServer, oldController, err := mockChartController()
require.Nil(t, err)
require.NotNil(t, chServer)
defer chServer.Close()
defer func() {
chartController = oldController
}()
project := models.Project{ project := models.Project{
Name: "project_for_test_deletable", Name: "project_for_test_deletable",
@ -398,3 +409,36 @@ func TestDeletable(t *testing.T) {
assert.Equal(t, http.StatusOK, code) assert.Equal(t, http.StatusOK, code)
assert.False(t, del) assert.False(t, del)
} }
//Provides a mock chart controller for deletable test cases
func mockChartController() (*httptest.Server, *chartserver.Controller, error) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/api/project_for_test_deletable/charts":
if r.Method == http.MethodGet {
w.Write([]byte("{}"))
return
}
}
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("not supported"))
}))
var oldController, newController *chartserver.Controller
url, err := url.Parse(mockServer.URL)
if err == nil {
newController, err = chartserver.NewController(url)
}
if err != nil {
mockServer.Close()
return nil, nil, err
}
//Override current controller and keep the old one for restoring
oldController = chartController
chartController = newController
return mockServer, oldController, nil
}