mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-22 10:15:35 +01:00
Delete all the versions of the specified chart
- add API route - add DELETE chart method in utility handler of chart controller - add UT case for the newly added methods - update swagger.yaml to refelct the new change Signed-off-by: Steven Zou <szou@vmware.com>
This commit is contained in:
parent
925f70a1ac
commit
34f19f437d
@ -2903,6 +2903,32 @@ paths:
|
||||
$ref: '#/definitions/InternalChartAPIError'
|
||||
'200':
|
||||
$ref: '#/definitions/ChartVersions'
|
||||
delete:
|
||||
summary: Delete all the versions of the specified chart
|
||||
description: Delete all the versions of the specified chart
|
||||
tags:
|
||||
- Products
|
||||
- Chart Repository
|
||||
parameters:
|
||||
- name: repo
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The project name
|
||||
- name: name
|
||||
in: path
|
||||
type: string
|
||||
required: true
|
||||
description: The chart name
|
||||
responses:
|
||||
'401':
|
||||
$ref: '#/definitions/UnauthorizedChartAPIError'
|
||||
'403':
|
||||
$ref: '#/definitions/ForbiddenChartAPIError'
|
||||
'500':
|
||||
$ref: '#/definitions/InternalChartAPIError'
|
||||
'200':
|
||||
description: The specified chart entry is successfully deleted.
|
||||
/chartrepo/{repo}/charts/{name}/{version}:
|
||||
get:
|
||||
summary: Get the specified chart version
|
||||
|
@ -138,6 +138,20 @@ func (cho *ChartOperator) GetChartList(content []byte) ([]*ChartInfo, error) {
|
||||
return chartList, nil
|
||||
}
|
||||
|
||||
//GetChartVersions returns the chart versions
|
||||
func (cho *ChartOperator) GetChartVersions(content []byte) (helm_repo.ChartVersions, error) {
|
||||
if content == nil || len(content) == 0 {
|
||||
return nil, errors.New("zero content")
|
||||
}
|
||||
|
||||
chartVersions := make(helm_repo.ChartVersions, 0)
|
||||
if err := json.Unmarshal(content, &chartVersions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chartVersions, nil
|
||||
}
|
||||
|
||||
//Get the latest and oldest chart versions
|
||||
func getTheTwoCharts(chartVersions helm_repo.ChartVersions) (latestChart *helm_repo.ChartVersion, oldestChart *helm_repo.ChartVersion) {
|
||||
if len(chartVersions) == 1 {
|
||||
|
@ -3,6 +3,7 @@ package chartserver
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -44,6 +45,28 @@ func NewChartClient(credentail *Credential) *ChartClient { //Create http client
|
||||
|
||||
//GetContent get the bytes from the specified url
|
||||
func (cc *ChartClient) GetContent(addr string) ([]byte, error) {
|
||||
response, err := cc.sendRequest(addr, http.MethodGet, nil, []int{http.StatusOK})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
//DeleteContent sends deleting request to the addr to delete content
|
||||
func (cc *ChartClient) DeleteContent(addr string) error {
|
||||
_, err := cc.sendRequest(addr, http.MethodDelete, nil, []int{http.StatusOK})
|
||||
return err
|
||||
}
|
||||
|
||||
//sendRequest sends requests to the addr with the specified spec
|
||||
func (cc *ChartClient) sendRequest(addr string, method string, body io.Reader, expectedCodes []int) (*http.Response, error) {
|
||||
if len(strings.TrimSpace(addr)) == 0 {
|
||||
return nil, errors.New("empty url is not allowed")
|
||||
}
|
||||
@ -53,7 +76,7 @@ func (cc *ChartClient) GetContent(addr string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("invalid url: %s", err.Error())
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, addr, nil)
|
||||
request, err := http.NewRequest(method, addr, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -68,19 +91,27 @@ func (cc *ChartClient) GetContent(addr string) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
isExpectedStatusCode := false
|
||||
for _, eCode := range expectedCodes {
|
||||
if eCode == response.StatusCode {
|
||||
isExpectedStatusCode = true
|
||||
break
|
||||
}
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
if !isExpectedStatusCode {
|
||||
content, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if err := extractError(content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to retrieve content from '%s' with error: %s", fullURI.Path, content)
|
||||
return nil, fmt.Errorf("%s '%s' failed with error: %s", method, fullURI.Path, content)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
return response, nil
|
||||
}
|
||||
|
@ -5,6 +5,13 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
helm_repo "k8s.io/helm/pkg/repo"
|
||||
)
|
||||
|
||||
const (
|
||||
maxDeletionThreads = 10
|
||||
)
|
||||
|
||||
//UtilityHandler provides utility methods
|
||||
@ -35,3 +42,99 @@ func (uh *UtilityHandler) GetChartsByNs(namespace string) ([]*ChartInfo, error)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
@ -42,3 +42,111 @@ func TestGetChartsByNs(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
`
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
|
||||
const (
|
||||
namespaceParam = ":repo"
|
||||
nameParam = ":name"
|
||||
defaultRepo = "library"
|
||||
rootUploadingEndpoint = "/api/chartrepo/charts"
|
||||
rootIndexEndpoint = "/chartrepo/index.yaml"
|
||||
@ -215,6 +216,21 @@ func (cra *ChartRepositoryAPI) UploadChartProvFile() {
|
||||
chartController.GetManipulationHandler().UploadProvenanceFile(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
||||
}
|
||||
|
||||
//DeleteChart deletes all the chart versions of the specified chart.
|
||||
func (cra *ChartRepositoryAPI) DeleteChart() {
|
||||
//Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelWrite) {
|
||||
return
|
||||
}
|
||||
|
||||
//Get other parameters from the request
|
||||
chartName := cra.GetStringFromPath(nameParam)
|
||||
|
||||
if err := chartController.GetUtilityHandler().DeleteChart(cra.namespace, chartName); err != nil {
|
||||
chartserver.WriteInternalError(cra.Ctx.ResponseWriter, err)
|
||||
}
|
||||
}
|
||||
|
||||
//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
|
||||
|
@ -137,6 +137,7 @@ func initRouters() {
|
||||
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")
|
||||
beego.Router("/api/chartrepo/:repo/charts", chartRepositoryAPIType, "get:ListCharts")
|
||||
beego.Router("/api/chartrepo/:repo/charts/:name", chartRepositoryAPIType, "get:ListChartVersions")
|
||||
beego.Router("/api/chartrepo/:repo/charts/:name", chartRepositoryAPIType, "delete:DeleteChart")
|
||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version", chartRepositoryAPIType, "get:GetChartVersion")
|
||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version", chartRepositoryAPIType, "delete:DeleteChartVersion")
|
||||
beego.Router("/api/chartrepo/:repo/charts", chartRepositoryAPIType, "post:UploadChartVersion")
|
||||
|
Loading…
Reference in New Issue
Block a user