mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-25 19:56:09 +01:00
Merge pull request #5759 from steven-zou/support_delete_chart
Delete all the versions of the specified chart
This commit is contained in:
commit
7752595313
@ -2911,6 +2911,32 @@ paths:
|
|||||||
$ref: '#/definitions/InternalChartAPIError'
|
$ref: '#/definitions/InternalChartAPIError'
|
||||||
'200':
|
'200':
|
||||||
$ref: '#/definitions/ChartVersions'
|
$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}:
|
/chartrepo/{repo}/charts/{name}/{version}:
|
||||||
get:
|
get:
|
||||||
summary: Get the specified chart version
|
summary: Get the specified chart version
|
||||||
|
@ -153,6 +153,20 @@ func (cho *ChartOperator) GetChartList(content []byte) ([]*ChartInfo, error) {
|
|||||||
return chartList, nil
|
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
|
//Get the latest and oldest chart versions
|
||||||
func getTheTwoCharts(chartVersions helm_repo.ChartVersions) (latestChart *helm_repo.ChartVersion, oldestChart *helm_repo.ChartVersion) {
|
func getTheTwoCharts(chartVersions helm_repo.ChartVersions) (latestChart *helm_repo.ChartVersion, oldestChart *helm_repo.ChartVersion) {
|
||||||
if len(chartVersions) == 1 {
|
if len(chartVersions) == 1 {
|
||||||
|
@ -3,6 +3,7 @@ package chartserver
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -44,6 +45,28 @@ func NewChartClient(credentail *Credential) *ChartClient { //Create http client
|
|||||||
|
|
||||||
//GetContent get the bytes from the specified url
|
//GetContent get the bytes from the specified url
|
||||||
func (cc *ChartClient) GetContent(addr string) ([]byte, error) {
|
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 {
|
if len(strings.TrimSpace(addr)) == 0 {
|
||||||
return nil, errors.New("empty url is not allowed")
|
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())
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -68,19 +91,27 @@ func (cc *ChartClient) GetContent(addr string) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := ioutil.ReadAll(response.Body)
|
isExpectedStatusCode := false
|
||||||
if err != nil {
|
for _, eCode := range expectedCodes {
|
||||||
return nil, err
|
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 {
|
if err := extractError(content); err != nil {
|
||||||
return nil, err
|
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"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
helm_repo "k8s.io/helm/pkg/repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxDeletionThreads = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
//UtilityHandler provides utility methods
|
//UtilityHandler provides utility methods
|
||||||
@ -35,3 +42,99 @@ func (uh *UtilityHandler) GetChartsByNs(namespace string) ([]*ChartInfo, error)
|
|||||||
|
|
||||||
return uh.chartOperator.GetChartList(content)
|
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))
|
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 (
|
const (
|
||||||
namespaceParam = ":repo"
|
namespaceParam = ":repo"
|
||||||
|
nameParam = ":name"
|
||||||
defaultRepo = "library"
|
defaultRepo = "library"
|
||||||
rootUploadingEndpoint = "/api/chartrepo/charts"
|
rootUploadingEndpoint = "/api/chartrepo/charts"
|
||||||
rootIndexEndpoint = "/chartrepo/index.yaml"
|
rootIndexEndpoint = "/chartrepo/index.yaml"
|
||||||
@ -215,6 +216,21 @@ func (cra *ChartRepositoryAPI) UploadChartProvFile() {
|
|||||||
chartController.GetManipulationHandler().UploadProvenanceFile(cra.Ctx.ResponseWriter, cra.Ctx.Request)
|
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
|
//Rewrite the incoming URL with the right backend URL pattern
|
||||||
//Remove 'chartrepo' from the endpoints of manipulation API
|
//Remove 'chartrepo' from the endpoints of manipulation API
|
||||||
//Remove 'chartrepo' from the endpoints of repository services
|
//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/health", chartRepositoryAPIType, "get:GetHealthStatus")
|
||||||
beego.Router("/api/chartrepo/:repo/charts", chartRepositoryAPIType, "get:ListCharts")
|
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, "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, "get:GetChartVersion")
|
||||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version", chartRepositoryAPIType, "delete:DeleteChartVersion")
|
beego.Router("/api/chartrepo/:repo/charts/:name/:version", chartRepositoryAPIType, "delete:DeleteChartVersion")
|
||||||
beego.Router("/api/chartrepo/:repo/charts", chartRepositoryAPIType, "post:UploadChartVersion")
|
beego.Router("/api/chartrepo/:repo/charts", chartRepositoryAPIType, "post:UploadChartVersion")
|
||||||
|
Loading…
Reference in New Issue
Block a user