mirror of https://github.com/goharbor/harbor.git
Merge 58ef0a56c0
into 4da6070872
This commit is contained in:
commit
abb3a20e2b
|
@ -15,6 +15,7 @@
|
|||
package gitlab
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
|
@ -228,6 +229,7 @@ func existPatterns(path string, patterns []string) bool {
|
|||
correct := false
|
||||
if len(patterns) > 0 {
|
||||
for _, pathPattern := range patterns {
|
||||
log.Debug("Checking pathPattern: ", pathPattern, " against path: ", path)
|
||||
if ok, _ := util.Match(strings.ToLower(pathPattern), strings.ToLower(path)); ok {
|
||||
correct = true
|
||||
break
|
||||
|
@ -238,3 +240,76 @@ func existPatterns(path string, patterns []string) bool {
|
|||
}
|
||||
return correct
|
||||
}
|
||||
|
||||
func (a *adapter) getProjectsByRepositoryName(repository string) ([]*Project, error) {
|
||||
repositoriesToTry := []string{repository}
|
||||
|
||||
components := strings.Split(repository, "/")
|
||||
for i := len(components) - 1; i >= 2; i-- {
|
||||
repositoriesToTry = append(repositoriesToTry, strings.Join(components[:i], "/"))
|
||||
}
|
||||
|
||||
for _, repo := range repositoriesToTry {
|
||||
projects, err := a.clientGitlabAPI.getProjectsByName(url.QueryEscape(repo))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(projects) > 0 {
|
||||
return projects, nil
|
||||
}
|
||||
}
|
||||
|
||||
return []*Project{}, nil
|
||||
}
|
||||
|
||||
func (a *adapter) DeleteManifest(repository, reference string) error {
|
||||
log.Errorf("DeleteManifest called with repository: %s, reference: %s", repository, reference)
|
||||
|
||||
projects, err := a.getProjectsByRepositoryName(repository)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get projects by pattern %s: %v", repository, err)
|
||||
}
|
||||
if len(projects) == 0 {
|
||||
log.Errorf("No projects found for pattern %s", repository)
|
||||
return errors.New("no projects found")
|
||||
}
|
||||
projectID := projects[0].ID
|
||||
|
||||
log.Debugf("Project ID: %d", projectID)
|
||||
|
||||
repositories, err := a.clientGitlabAPI.getRepositories(projectID)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get repositories for project %s: %v", repository, err)
|
||||
}
|
||||
if len(repositories) == 0 {
|
||||
log.Errorf("No repositories found for project %s", repository)
|
||||
return errors.New("no repositories found")
|
||||
}
|
||||
|
||||
// Filter by hand because the API does not support filtering by repository name
|
||||
repositoryID := int64(-1)
|
||||
for _, repo := range repositories {
|
||||
if repo.Path == repository {
|
||||
log.Debugf("Found repository ID: %d for path: %s", repositoryID, repo.Path)
|
||||
repositoryID = repo.ID
|
||||
break
|
||||
}
|
||||
log.Debugf("Skipping repository path=%s and id=%d", repo.Path, repo.ID)
|
||||
}
|
||||
|
||||
if repositoryID == -1 {
|
||||
log.Errorf("No repository found for path %s", repository)
|
||||
return errors.New("no repository found")
|
||||
}
|
||||
|
||||
log.Debugf("Deleting tag %s from repository %s with ID %d", reference, repository, repositoryID)
|
||||
|
||||
err = a.clientGitlabAPI.deleteTag(projectID, repositoryID, reference)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to delete tag %s from repository %s with ID %d: %v", reference, repository, repositoryID, err)
|
||||
return err
|
||||
}
|
||||
log.Debugf("Tag %s deleted successfully from repository %s with ID %d", reference, repository, repositoryID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"net/http/httptest"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -28,11 +27,12 @@ func mustWriteHTTPResponse(t *testing.T, w io.Writer, fixturePath string) {
|
|||
}
|
||||
func getServer(t *testing.T) *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/v2/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Www-Authenticate", "Bearer realm=\"http://"+r.Host+"/jwt/auth\",service=\"container_registry\"")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/v4/projects", func(w http.ResponseWriter, r *http.Request) {
|
||||
search := r.URL.Query().Get("search")
|
||||
w.Header().Set("X-Next-Page", "")
|
||||
|
@ -40,40 +40,42 @@ func getServer(t *testing.T) *httptest.Server {
|
|||
switch search {
|
||||
case "library/dev-docker", "library", "library/", "dev-docker/", "dev-docker":
|
||||
mustWriteHTTPResponse(t, w, "testdata/projects/dev-docker.json")
|
||||
break
|
||||
case "", "library/dockers":
|
||||
mustWriteHTTPResponse(t, w, "testdata/projects/all.json")
|
||||
break
|
||||
default:
|
||||
w.Header().Set("X-Next-Page", "")
|
||||
w.Write([]byte(`[]`))
|
||||
break
|
||||
}
|
||||
|
||||
})
|
||||
for projectID := 1; projectID <= 5; projectID++ {
|
||||
mux.HandleFunc("/api/v4/projects/"+strconv.Itoa(projectID)+"/registry/repositories", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
mux.HandleFunc("/api/v4/projects/", func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
|
||||
reRepo := regexp.MustCompile(`/api/v4/projects/(\d+)/registry/repositories/?$`)
|
||||
if match := reRepo.FindStringSubmatch(path); match != nil {
|
||||
projectID := match[1]
|
||||
w.Header().Set("X-Next-Page", "")
|
||||
re := regexp.MustCompile(`projects/(?P<id>\d+)/registry`)
|
||||
match := re.FindStringSubmatch(r.RequestURI)
|
||||
mustWriteHTTPResponse(t, w, "testdata/repositories/"+match[1]+".json")
|
||||
|
||||
})
|
||||
for repositoryID := 1; repositoryID <= 5; repositoryID++ {
|
||||
mux.HandleFunc("/api/v4/projects/"+strconv.Itoa(projectID)+"/registry/repositories/"+strconv.Itoa(repositoryID)+"1/tags", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("X-Next-Page", "")
|
||||
re := regexp.MustCompile(`repositories/(?P<id>\d+)/tags`)
|
||||
match := re.FindStringSubmatch(r.RequestURI)
|
||||
mustWriteHTTPResponse(t, w, "testdata/tags/"+match[1]+".json")
|
||||
|
||||
})
|
||||
|
||||
mustWriteHTTPResponse(t, w, "testdata/repositories/"+projectID+".json")
|
||||
return
|
||||
}
|
||||
}
|
||||
server := httptest.NewServer(mux)
|
||||
return server
|
||||
|
||||
reTags := regexp.MustCompile(`/api/v4/projects/(\d+)/registry/repositories/(\d+)/tags$`)
|
||||
if match := reTags.FindStringSubmatch(path); match != nil {
|
||||
repoID := match[2]
|
||||
w.Header().Set("X-Next-Page", "")
|
||||
mustWriteHTTPResponse(t, w, "testdata/tags/"+repoID+".json")
|
||||
return
|
||||
}
|
||||
|
||||
reDeleteTag := regexp.MustCompile(`/api/v4/projects/(\d+)/registry/repositories/(\d+)/tags/([^/]+)$`)
|
||||
if match := reDeleteTag.FindStringSubmatch(path); match != nil {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
http.NotFound(w, r)
|
||||
})
|
||||
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func getAdapter(t *testing.T) adp.Adapter {
|
||||
|
@ -132,3 +134,36 @@ func TestFetchImages(t *testing.T) {
|
|||
require.Equal(t, 1, len(resources))
|
||||
require.Equal(t, 2, len(resources[0].Metadata.Vtags))
|
||||
}
|
||||
|
||||
func TestDeleteManifest(t *testing.T) {
|
||||
assertions := assert.New(t)
|
||||
ad := getAdapter(t)
|
||||
adapter := ad.(*adapter)
|
||||
|
||||
t.Run("successful deletion", func(t *testing.T) {
|
||||
err := adapter.DeleteManifest("library/dockers", "harbor")
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("successful deletion 1 level nested registry", func(t *testing.T) {
|
||||
err := adapter.DeleteManifest("library/dockers/harbor", "v0.1.1")
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("successful deletion with dev-docker", func(t *testing.T) {
|
||||
err := adapter.DeleteManifest("library/dev-docker", "latest")
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("no projects found", func(t *testing.T) {
|
||||
err := adapter.DeleteManifest("nonexistent/repository", "v1.0.0")
|
||||
require.NotNil(t, err)
|
||||
assertions.Contains(err.Error(), "no projects found")
|
||||
})
|
||||
|
||||
t.Run("no repository found in project", func(t *testing.T) {
|
||||
err := adapter.DeleteManifest("library/nonexistent-repo", "v1.0.0")
|
||||
require.NotNil(t, err)
|
||||
assertions.Contains(err.Error(), "no projects found")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ func (c *Client) getProjectsByName(name string) ([]*Project, error) {
|
|||
}
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func (c *Client) getRepositories(projectID int64) ([]*Repository, error) {
|
||||
var repositories []*Repository
|
||||
urlAPI := fmt.Sprintf("%s/api/v4/projects/%d/registry/repositories?per_page=50", c.url, projectID)
|
||||
|
@ -109,6 +110,26 @@ func (c *Client) getTags(projectID int64, repositoryID int64) ([]*Tag, error) {
|
|||
return tags, nil
|
||||
}
|
||||
|
||||
func (c *Client) deleteTag(projectID int64, repositoryID int64, tagName string) error {
|
||||
urlAPI := fmt.Sprintf("%s/api/v4/projects/%d/registry/repositories/%d/tags/%s", c.url, projectID, repositoryID, tagName)
|
||||
req, err := c.newRequest(http.MethodDelete, urlAPI, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to delete tag: %v", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("failed to delete tag with status code %d: %s", resp.StatusCode, string(data))
|
||||
}
|
||||
log.Debugf("Successfully deleted tag in project %d and repository %d", projectID, repositoryID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAndIteratePagination iterates the pagination header and returns all resources
|
||||
// The parameter "v" must be a pointer to a slice
|
||||
func (c *Client) GetAndIteratePagination(endpoint string, v any) error {
|
||||
|
|
|
@ -84,3 +84,158 @@ func TestProjects(t *testing.T) {
|
|||
require.Nil(t, e)
|
||||
assert.Equal(t, 1, len(projects))
|
||||
}
|
||||
|
||||
func TestDeleteTag(t *testing.T) {
|
||||
t.Run("successful deletion", func(t *testing.T) {
|
||||
server := test.NewServer(&test.RequestHandlerMapping{
|
||||
Method: http.MethodDelete,
|
||||
Pattern: "/api/v4/projects/123/registry/repositories/456/tags/v1.0.0",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := &Client{
|
||||
url: server.URL,
|
||||
username: "test",
|
||||
token: "test",
|
||||
client: common_http.NewClient(
|
||||
&http.Client{
|
||||
Transport: common_http.GetHTTPTransport(common_http.WithInsecure(true)),
|
||||
}),
|
||||
}
|
||||
|
||||
err := client.deleteTag(123, 456, "v1.0.0")
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("deletion with 204 status", func(t *testing.T) {
|
||||
server := test.NewServer(&test.RequestHandlerMapping{
|
||||
Method: http.MethodDelete,
|
||||
Pattern: "/api/v4/projects/123/registry/repositories/456/tags/v1.0.0",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := &Client{
|
||||
url: server.URL,
|
||||
username: "test",
|
||||
token: "test",
|
||||
client: common_http.NewClient(
|
||||
&http.Client{
|
||||
Transport: common_http.GetHTTPTransport(common_http.WithInsecure(true)),
|
||||
}),
|
||||
}
|
||||
|
||||
err := client.deleteTag(123, 456, "v1.0.0")
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("deletion with 404 not found", func(t *testing.T) {
|
||||
server := test.NewServer(&test.RequestHandlerMapping{
|
||||
Method: http.MethodDelete,
|
||||
Pattern: "/api/v4/projects/123/registry/repositories/456/tags/v1.0.0",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte(`{"message": "Tag not found"}`))
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := &Client{
|
||||
url: server.URL,
|
||||
username: "test",
|
||||
token: "test",
|
||||
client: common_http.NewClient(
|
||||
&http.Client{
|
||||
Transport: common_http.GetHTTPTransport(common_http.WithInsecure(true)),
|
||||
}),
|
||||
}
|
||||
|
||||
err := client.deleteTag(123, 456, "v1.0.0")
|
||||
require.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to delete tag with status code 404")
|
||||
assert.Contains(t, err.Error(), "Tag not found")
|
||||
})
|
||||
|
||||
t.Run("deletion with 403 forbidden", func(t *testing.T) {
|
||||
server := test.NewServer(&test.RequestHandlerMapping{
|
||||
Method: http.MethodDelete,
|
||||
Pattern: "/api/v4/projects/123/registry/repositories/456/tags/v1.0.0",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(`{"message": "Access denied"}`))
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := &Client{
|
||||
url: server.URL,
|
||||
username: "test",
|
||||
token: "test",
|
||||
client: common_http.NewClient(
|
||||
&http.Client{
|
||||
Transport: common_http.GetHTTPTransport(common_http.WithInsecure(true)),
|
||||
}),
|
||||
}
|
||||
|
||||
err := client.deleteTag(123, 456, "v1.0.0")
|
||||
require.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to delete tag with status code 403")
|
||||
assert.Contains(t, err.Error(), "Access denied")
|
||||
})
|
||||
|
||||
t.Run("deletion with 500 internal server error", func(t *testing.T) {
|
||||
server := test.NewServer(&test.RequestHandlerMapping{
|
||||
Method: http.MethodDelete,
|
||||
Pattern: "/api/v4/projects/123/registry/repositories/456/tags/v1.0.0",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(`{"message": "Internal server error"}`))
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := &Client{
|
||||
url: server.URL,
|
||||
username: "test",
|
||||
token: "test",
|
||||
client: common_http.NewClient(
|
||||
&http.Client{
|
||||
Transport: common_http.GetHTTPTransport(common_http.WithInsecure(true)),
|
||||
}),
|
||||
}
|
||||
|
||||
err := client.deleteTag(123, 456, "v1.0.0")
|
||||
require.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to delete tag with status code 500")
|
||||
assert.Contains(t, err.Error(), "Internal server error")
|
||||
})
|
||||
|
||||
t.Run("tag name with special characters", func(t *testing.T) {
|
||||
server := test.NewServer(&test.RequestHandlerMapping{
|
||||
Method: http.MethodDelete,
|
||||
Pattern: "/api/v4/projects/123/registry/repositories/456/tags/v1.0.0-alpha.1",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := &Client{
|
||||
url: server.URL,
|
||||
username: "test",
|
||||
token: "test",
|
||||
client: common_http.NewClient(
|
||||
&http.Client{
|
||||
Transport: common_http.GetHTTPTransport(common_http.WithInsecure(true)),
|
||||
}),
|
||||
}
|
||||
|
||||
err := client.deleteTag(123, 456, "v1.0.0-alpha.1")
|
||||
require.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,5 +6,19 @@
|
|||
"project_id": 1,
|
||||
"location": "registry.gitlab.com/library/dockers",
|
||||
"created_at": "2019-01-17T09:47:07.504Z"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "",
|
||||
"path": "library/dockers/harbor",
|
||||
"project_id": 1,
|
||||
"location": "registry.gitlab.com/library/dockers/harbor"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "",
|
||||
"path": "library/dockers/harbor/jobservice",
|
||||
"project_id": 1,
|
||||
"location": "registry.gitlab.com/library/dockers/harbor/jobservice"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"name": "latest",
|
||||
"path": "library/docker/harbor:latest",
|
||||
"location": "registry.gitlab.com/library/docker/harbor:latest"
|
||||
},
|
||||
{
|
||||
"name": "v1.0.0",
|
||||
"path": "library/docker/harbor:v1.0.0",
|
||||
"location": "registry.gitlab.com/library/docker/harbor:v1.0.0"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,7 @@
|
|||
[
|
||||
{
|
||||
"name": "v0.1.2",
|
||||
"path": "library/docker/harbor/jobservice:v0.1.2",
|
||||
"location": "registry.gitlab.com/library/docker/harbor/jobservice:v0.1.2"
|
||||
}
|
||||
]
|
Loading…
Reference in New Issue