mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-26 18:48:02 +01:00
commit
db24cbe25a
@ -1131,6 +1131,33 @@ paths:
|
||||
$ref: '#/definitions/DetailedTag'
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
post:
|
||||
summary: Retag an image
|
||||
description: >
|
||||
This endpoint tags an existing image with another tag in this repo, source images
|
||||
can be in different repos or projects.
|
||||
parameters:
|
||||
- name: request
|
||||
in: body
|
||||
description: Request to give source image and target tag.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/RetagReq'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Image retag successfully.
|
||||
'400':
|
||||
description: Invalid image values provided.
|
||||
'401':
|
||||
description: User has no permission to the source project or destination project.
|
||||
'404':
|
||||
description: Project or repository not found.
|
||||
'409':
|
||||
description: Target tag already exists.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/repositories/{repo_name}/tags/{tag}/labels':
|
||||
get:
|
||||
summary: Get labels of an image.
|
||||
@ -2966,6 +2993,18 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/SearchResult'
|
||||
RetagReq:
|
||||
type: object
|
||||
properties:
|
||||
tag:
|
||||
description: new tag to be created
|
||||
type: string
|
||||
src_image:
|
||||
description: Source image to be retagged, e.g. 'stage/app:v1.0'
|
||||
type: string
|
||||
override:
|
||||
description: If target tag already exists, whether to override it
|
||||
type: boolean
|
||||
SearchRepository:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -113,6 +113,12 @@ func GetProjectByName(name string) (*models.Project, error) {
|
||||
return &p[0], nil
|
||||
}
|
||||
|
||||
// ProjectExistsByName returns whether the project exists according to its name.
|
||||
func ProjectExistsByName(name string) bool {
|
||||
o := GetOrmer()
|
||||
return o.QueryTable("project").Filter("name", name).Exist()
|
||||
}
|
||||
|
||||
// GetTotalOfProjects returns the total count of projects
|
||||
// according to the query conditions
|
||||
func GetTotalOfProjects(query *models.ProjectQueryParam) (int64, error) {
|
||||
|
@ -235,3 +235,30 @@ func TestGetRolesByLDAPGroup(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjetExistsByName(t *testing.T) {
|
||||
name := "project_exist_by_name_test"
|
||||
exist := ProjectExistsByName(name)
|
||||
if exist {
|
||||
t.Errorf("project %s expected to be not exist", name)
|
||||
}
|
||||
|
||||
project := models.Project{
|
||||
OwnerID: currentUser.UserID,
|
||||
Name: name,
|
||||
}
|
||||
id, err := AddProject(project)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add project: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := delProjPermanent(id); err != nil {
|
||||
t.Errorf("failed to clear up project %d: %v", id, err)
|
||||
}
|
||||
}()
|
||||
|
||||
exist = ProjectExistsByName(name)
|
||||
if !exist {
|
||||
t.Errorf("project %s expected to be exist", name)
|
||||
}
|
||||
}
|
||||
|
52
src/common/models/retag.go
Normal file
52
src/common/models/retag.go
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RetagRequest gives the source image and target image of retag
|
||||
type RetagRequest struct {
|
||||
Tag string `json:"tag"` // The new tag
|
||||
SrcImage string `json:"src_image"` // Source images in format <project>/<repo>:<reference>
|
||||
Override bool `json:"override"` // If target tag exists, whether override it
|
||||
}
|
||||
|
||||
// Image holds each part (project, repo, tag) of an image name
|
||||
type Image struct {
|
||||
Project string
|
||||
Repo string
|
||||
Tag string
|
||||
}
|
||||
|
||||
// ParseImage parses an image name such as 'library/app:v1.0' to a structure with
|
||||
// project, repo, and tag fields
|
||||
func ParseImage(image string) (*Image, error) {
|
||||
repo := strings.SplitN(image, "/", 2)
|
||||
if len(repo) < 2 {
|
||||
return nil, fmt.Errorf("Unable to parse image from string: %s", image)
|
||||
}
|
||||
i := strings.SplitN(repo[1], ":", 2)
|
||||
res := &Image{
|
||||
Project: repo[0],
|
||||
Repo: i[0],
|
||||
}
|
||||
if len(i) == 2 {
|
||||
res.Tag = i[1]
|
||||
}
|
||||
return res, nil
|
||||
}
|
75
src/common/models/retag_test.go
Normal file
75
src/common/models/retag_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseImage(t *testing.T) {
|
||||
cases := []struct {
|
||||
Input string
|
||||
Expected *Image
|
||||
Valid bool
|
||||
}{
|
||||
{
|
||||
Input: "library/busybox",
|
||||
Expected: &Image{
|
||||
Project: "library",
|
||||
Repo: "busybox",
|
||||
Tag: "",
|
||||
},
|
||||
Valid: true,
|
||||
},
|
||||
{
|
||||
Input: "library/busybox:v1.0",
|
||||
Expected: &Image{
|
||||
Project: "library",
|
||||
Repo: "busybox",
|
||||
Tag: "v1.0",
|
||||
},
|
||||
Valid: true,
|
||||
},
|
||||
{
|
||||
Input: "library/busybox:sha256:9e2c9d5f44efbb6ee83aecd17a120c513047d289d142ec5738c9f02f9b24ad07",
|
||||
Expected: &Image{
|
||||
Project: "library",
|
||||
Repo: "busybox",
|
||||
Tag: "sha256:9e2c9d5f44efbb6ee83aecd17a120c513047d289d142ec5738c9f02f9b24ad07",
|
||||
},
|
||||
Valid: true,
|
||||
},
|
||||
{
|
||||
Input: "busybox/v1.0",
|
||||
Valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
output, err := ParseImage(c.Input)
|
||||
if c.Valid {
|
||||
if !reflect.DeepEqual(output, c.Expected) {
|
||||
assert.Equal(t, c.Expected, output)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse image %s", c.Input)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -38,6 +38,17 @@ type tokenGenerator interface {
|
||||
generate(scopes []*token.ResourceActions, endpoint string) (*models.Token, error)
|
||||
}
|
||||
|
||||
// UserAgentModifier adds the "User-Agent" header to the request
|
||||
type UserAgentModifier struct {
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
// Modify adds user-agent header to the request
|
||||
func (u *UserAgentModifier) Modify(req *http.Request) error {
|
||||
req.Header.Set(http.CanonicalHeaderKey("User-Agent"), u.UserAgent)
|
||||
return nil
|
||||
}
|
||||
|
||||
// tokenAuthorizer implements registry.Modifier interface. It parses scopses
|
||||
// from the request, generates authentication token and modifies the requset
|
||||
// by adding the token
|
||||
@ -246,7 +257,7 @@ func ping(client *http.Client, endpoint string) (string, string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
log.Warningf("schemes %v are unsupportted", challenges)
|
||||
log.Warningf("Schemas %v are unsupported", challenges)
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
|
@ -206,3 +206,17 @@ func TestModifyOfStandardTokenAuthorizer(t *testing.T) {
|
||||
tk := req.Header.Get("Authorization")
|
||||
assert.Equal(t, strings.ToLower("Bearer "+token.Token), strings.ToLower(tk))
|
||||
}
|
||||
|
||||
func TestUserAgentModifier(t *testing.T) {
|
||||
agent := "harbor-registry-client"
|
||||
modifier := &UserAgentModifier{
|
||||
UserAgent: agent,
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, "http://registry/v2/", nil)
|
||||
require.Nil(t, err)
|
||||
modifier.Modify(req)
|
||||
actual := req.Header.Get("User-Agent")
|
||||
if actual != agent {
|
||||
t.Errorf("expect request to have header User-Agent=%s, but got User-Agent=%s", agent, actual)
|
||||
}
|
||||
}
|
||||
|
@ -258,6 +258,28 @@ func (r *Repository) DeleteManifest(digest string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// MountBlob ...
|
||||
func (r *Repository) MountBlob(digest, from string) error {
|
||||
req, err := http.NewRequest("POST", buildMountBlobURL(r.Endpoint.String(), r.Name, digest, from), nil)
|
||||
req.Header.Set(http.CanonicalHeaderKey("Content-Length"), "0")
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
defer resp.Body.Close()
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("status %d, body: %s", resp.StatusCode, string(b))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTag ...
|
||||
func (r *Repository) DeleteTag(tag string) error {
|
||||
digest, exist, err := r.ManifestExist(tag)
|
||||
@ -462,6 +484,10 @@ func buildBlobURL(endpoint, repoName, reference string) string {
|
||||
return fmt.Sprintf("%s/v2/%s/blobs/%s", endpoint, repoName, reference)
|
||||
}
|
||||
|
||||
func buildMountBlobURL(endpoint, repoName, digest, from string) string {
|
||||
return fmt.Sprintf("%s/v2/%s/blobs/uploads/?mount=%s&from=%s", endpoint, repoName, digest, from)
|
||||
}
|
||||
|
||||
func buildInitiateBlobUploadURL(endpoint, repoName string) string {
|
||||
return fmt.Sprintf("%s/v2/%s/blobs/uploads/", endpoint, repoName)
|
||||
}
|
||||
|
@ -422,3 +422,37 @@ func TestBuildMonolithicBlobUploadURL(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, expected, url)
|
||||
}
|
||||
|
||||
func TestBuildMountBlobURL(t *testing.T) {
|
||||
endpoint := "http://192.169.0.1"
|
||||
repoName := "library/hello-world"
|
||||
digest := "sha256:ef15416724f6e2d5d5b422dc5105add931c1f2a45959cd4993e75e47957b3b55"
|
||||
from := "library/hi-world"
|
||||
expected := fmt.Sprintf("%s/v2/%s/blobs/uploads/?mount=%s&from=%s", endpoint, repoName, digest, from)
|
||||
|
||||
actual := buildMountBlobURL(endpoint, repoName, digest, from)
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestMountBlob(t *testing.T) {
|
||||
mountHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
server := test.NewServer(
|
||||
&test.RequestHandlerMapping{
|
||||
Method: "POST",
|
||||
Pattern: fmt.Sprintf("/v2/%s/blobs/uploads/", repository),
|
||||
Handler: mountHandler,
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client, err := newRepository(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client for repository: %v", err)
|
||||
}
|
||||
|
||||
if err = client.MountBlob(digest, "library/hi-world"); err != nil {
|
||||
t.Fatalf("failed to mount blob: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ func init() {
|
||||
beego.Router("/api/repositories/*/tags/:tag/labels", &RepositoryLabelAPI{}, "get:GetOfImage;post:AddToImage")
|
||||
beego.Router("/api/repositories/*/tags/:tag/labels/:id([0-9]+", &RepositoryLabelAPI{}, "delete:RemoveFromImage")
|
||||
beego.Router("/api/repositories/*/tags/:tag", &RepositoryAPI{}, "delete:Delete;get:GetTag")
|
||||
beego.Router("/api/repositories/*/tags", &RepositoryAPI{}, "get:GetTags")
|
||||
beego.Router("/api/repositories/*/tags", &RepositoryAPI{}, "get:GetTags;post:Retag")
|
||||
beego.Router("/api/repositories/*/tags/:tag/manifest", &RepositoryAPI{}, "get:GetManifests")
|
||||
beego.Router("/api/repositories/*/signatures", &RepositoryAPI{}, "get:GetSignatures")
|
||||
beego.Router("/api/repositories/top", &RepositoryAPI{}, "get:GetTopRepos")
|
||||
@ -618,6 +618,19 @@ func (a testapi) GetReposTags(authInfo usrInfo, repoName string) (int, interface
|
||||
return http.StatusOK, result, nil
|
||||
}
|
||||
|
||||
// RetagImage retag image to another tag
|
||||
func (a testapi) RetagImage(authInfo usrInfo, repoName string, retag *apilib.Retag) (int, error) {
|
||||
_sling := sling.New().Post(a.basePath)
|
||||
|
||||
path := fmt.Sprintf("/api/repositories/%s/tags", repoName)
|
||||
|
||||
_sling = _sling.Path(path)
|
||||
_sling = _sling.BodyJSON(retag)
|
||||
|
||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
return httpStatusCode, err
|
||||
}
|
||||
|
||||
// Get manifests of a relevant repository
|
||||
func (a testapi) GetReposManifests(authInfo usrInfo, repoName string, tag string) (int, error) {
|
||||
_sling := sling.New().Get(a.basePath)
|
||||
|
@ -424,6 +424,82 @@ func (ra *RepositoryAPI) GetTag() {
|
||||
ra.ServeJSON()
|
||||
}
|
||||
|
||||
// Retag tags an existing image to another tag in this repo, the source image is specified by request body.
|
||||
func (ra *RepositoryAPI) Retag() {
|
||||
if !ra.SecurityCtx.IsAuthenticated() {
|
||||
ra.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
repoName := ra.GetString(":splat")
|
||||
request := models.RetagRequest{}
|
||||
ra.DecodeJSONReq(&request)
|
||||
srcImage, err := models.ParseImage(request.SrcImage)
|
||||
if err != nil {
|
||||
ra.HandleBadRequest(fmt.Sprintf("invalid src image string '%s', should in format '<project>/<repo>:<tag>'", request.SrcImage))
|
||||
return
|
||||
}
|
||||
|
||||
// Check whether source image exists
|
||||
exist, _, err := ra.checkExistence(fmt.Sprintf("%s/%s", srcImage.Project, srcImage.Repo), srcImage.Tag)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("check existence of %s error: %v", request.SrcImage, err))
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
ra.HandleNotFound(fmt.Sprintf("image %s not exist", request.SrcImage))
|
||||
return
|
||||
}
|
||||
|
||||
// Check whether target project exists
|
||||
project, repo := utils.ParseRepository(repoName)
|
||||
exist, err = ra.ProjectMgr.Exists(project)
|
||||
if err != nil {
|
||||
ra.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %s", project), err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
ra.HandleNotFound(fmt.Sprintf("project %s not found", project))
|
||||
return
|
||||
}
|
||||
|
||||
// If override not allowed, check whether target tag already exists
|
||||
if !request.Override {
|
||||
exist, _, err := ra.checkExistence(repoName, request.Tag)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("check existence of %s:%s error: %v", repoName, request.Tag, err))
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
ra.HandleConflict(fmt.Sprintf("tag '%s' already existed for '%s'", request.Tag, repoName))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether use has read permission to source project
|
||||
if !ra.SecurityCtx.HasReadPerm(srcImage.Project) {
|
||||
log.Errorf("user has no read permission to project '%s'", srcImage.Project)
|
||||
ra.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
// Check whether user has write permission to target project
|
||||
if !ra.SecurityCtx.HasWritePerm(project) {
|
||||
log.Errorf("user has no write permission to project '%s'", project)
|
||||
ra.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
// Retag the image
|
||||
if err = coreutils.Retag(srcImage, &models.Image{
|
||||
Project: project,
|
||||
Repo: repo,
|
||||
Tag: request.Tag,
|
||||
}); err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("%v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// GetTags returns tags of a repository
|
||||
func (ra *RepositoryAPI) GetTags() {
|
||||
repoName := ra.GetString(":splat")
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/project"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/tests/apitests/apilib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -348,3 +349,96 @@ func TestPutOfRepository(t *testing.T) {
|
||||
require.NotNil(t, repository)
|
||||
assert.Equal(t, desc.Description, repository.Description)
|
||||
}
|
||||
|
||||
func TestRetag(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
repo := "library/hello-world"
|
||||
|
||||
fmt.Println("Testing Image Retag API")
|
||||
// -------------------case 1 : response code = 200------------------------//
|
||||
fmt.Println("case 1 : response code = 200")
|
||||
retagReq := &apilib.Retag{
|
||||
Tag: "prd",
|
||||
SrcImage: "library/hello-world:latest",
|
||||
Override: true,
|
||||
}
|
||||
code, err := apiTest.RetagImage(*admin, repo, retagReq)
|
||||
if err != nil {
|
||||
t.Errorf("failed to retag: %v", err)
|
||||
} else {
|
||||
assert.Equal(int(200), code, "response code should be 200")
|
||||
}
|
||||
|
||||
// -------------------case 2 : response code = 400------------------------//
|
||||
fmt.Println("case 2 : response code = 400: invalid image value provided")
|
||||
retagReq = &apilib.Retag{
|
||||
Tag: "prd",
|
||||
SrcImage: "hello-world:latest",
|
||||
Override: true,
|
||||
}
|
||||
httpStatusCode, err := apiTest.RetagImage(*admin, repo, retagReq)
|
||||
if err != nil {
|
||||
t.Errorf("failed to retag: %v", err)
|
||||
} else {
|
||||
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
|
||||
}
|
||||
|
||||
// -------------------case 3 : response code = 404------------------------//
|
||||
fmt.Println("case 3 : response code = 404: source image not exist")
|
||||
retagReq = &apilib.Retag{
|
||||
Tag: "prd",
|
||||
SrcImage: "release/hello-world:notexist",
|
||||
Override: true,
|
||||
}
|
||||
httpStatusCode, err = apiTest.RetagImage(*admin, repo, retagReq)
|
||||
if err != nil {
|
||||
t.Errorf("failed to retag: %v", err)
|
||||
} else {
|
||||
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
|
||||
}
|
||||
|
||||
// -------------------case 4 : response code = 404------------------------//
|
||||
fmt.Println("case 4 : response code = 404: target project not exist")
|
||||
retagReq = &apilib.Retag{
|
||||
Tag: "prd",
|
||||
SrcImage: "library/hello-world:latest",
|
||||
Override: true,
|
||||
}
|
||||
httpStatusCode, err = apiTest.RetagImage(*admin, "nonexist/hello-world", retagReq)
|
||||
if err != nil {
|
||||
t.Errorf("failed to retag: %v", err)
|
||||
} else {
|
||||
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
|
||||
}
|
||||
|
||||
// -------------------case 5 : response code = 401------------------------//
|
||||
fmt.Println("case 5 : response code = 401, unathorized")
|
||||
retagReq = &apilib.Retag{
|
||||
Tag: "prd",
|
||||
SrcImage: "library/hello-world:latest",
|
||||
Override: true,
|
||||
}
|
||||
httpStatusCode, err = apiTest.RetagImage(*unknownUsr, repo, retagReq)
|
||||
if err != nil {
|
||||
t.Errorf("failed to retag: %v", err)
|
||||
} else {
|
||||
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 401")
|
||||
}
|
||||
|
||||
// -------------------case 6 : response code = 409------------------------//
|
||||
fmt.Println("case 6 : response code = 409, conflict")
|
||||
retagReq = &apilib.Retag{
|
||||
Tag: "latest",
|
||||
SrcImage: "library/hello-world:latest",
|
||||
Override: false,
|
||||
}
|
||||
httpStatusCode, err = apiTest.RetagImage(*admin, repo, retagReq)
|
||||
if err != nil {
|
||||
t.Errorf("failed to retag: %v", err)
|
||||
} else {
|
||||
assert.Equal(int(409), httpStatusCode, "httpStatusCode should be 409")
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ func initRouters() {
|
||||
beego.Router("/api/repositories/*/tags/:tag", &api.RepositoryAPI{}, "delete:Delete;get:GetTag")
|
||||
beego.Router("/api/repositories/*/tags/:tag/labels", &api.RepositoryLabelAPI{}, "get:GetOfImage;post:AddToImage")
|
||||
beego.Router("/api/repositories/*/tags/:tag/labels/:id([0-9]+)", &api.RepositoryLabelAPI{}, "delete:RemoveFromImage")
|
||||
beego.Router("/api/repositories/*/tags", &api.RepositoryAPI{}, "get:GetTags")
|
||||
beego.Router("/api/repositories/*/tags", &api.RepositoryAPI{}, "get:GetTags;post:Retag")
|
||||
beego.Router("/api/repositories/*/tags/:tag/scan", &api.RepositoryAPI{}, "post:ScanImage")
|
||||
beego.Router("/api/repositories/*/tags/:tag/vulnerability/details", &api.RepositoryAPI{}, "Get:VulnerabilityDetails")
|
||||
beego.Router("/api/repositories/*/tags/:tag/manifest", &api.RepositoryAPI{}, "get:GetManifests")
|
||||
|
95
src/core/utils/retag.go
Normal file
95
src/core/utils/retag.go
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/registry"
|
||||
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
)
|
||||
|
||||
// Retag tags an image to another
|
||||
func Retag(srcImage, destImage *models.Image) error {
|
||||
isSameRepo := getRepoName(srcImage) == getRepoName(destImage)
|
||||
srcClient, err := NewRepositoryClientForUI("harbor-ui", getRepoName(srcImage))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destClient := srcClient
|
||||
if !isSameRepo {
|
||||
destClient, err = NewRepositoryClientForUI("harbor-ui", getRepoName(destImage))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, exist, err := srcClient.ManifestExist(srcImage.Tag)
|
||||
if err != nil {
|
||||
log.Errorf("check existence of manifest '%s:%s' error: %v", srcClient.Name, srcImage.Tag, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !exist {
|
||||
log.Errorf("source image %s:%s not found", srcClient.Name, srcImage.Tag)
|
||||
return fmt.Errorf("image %s:%s not found", srcClient.Name, srcImage.Tag)
|
||||
}
|
||||
|
||||
accepted := []string{schema1.MediaTypeManifest, schema2.MediaTypeManifest}
|
||||
digest, mediaType, payload, err := srcClient.PullManifest(srcImage.Tag, accepted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifest, _, err := registry.UnMarshal(mediaType, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
destDigest, exist, err := destClient.ManifestExist(destImage.Tag)
|
||||
if err != nil {
|
||||
log.Errorf("check existence of manifest '%s:%s' error: %v", destClient.Name, destImage.Tag, err)
|
||||
return err
|
||||
}
|
||||
if exist && destDigest == digest {
|
||||
log.Infof("manifest of '%s:%s' already exist", destClient.Name, destImage.Tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isSameRepo {
|
||||
for _, descriptor := range manifest.References() {
|
||||
err := destClient.MountBlob(descriptor.Digest.String(), srcClient.Name)
|
||||
if err != nil {
|
||||
log.Errorf("mount blob '%s' error: %v", descriptor.Digest.String(), err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = destClient.PushManifest(destImage.Tag, mediaType, payload); err != nil {
|
||||
log.Errorf("push manifest '%s:%s' error: %v", destClient.Name, destImage.Tag, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRepoName(image *models.Image) string {
|
||||
return fmt.Sprintf("%s/%s", image.Project, image.Repo)
|
||||
}
|
@ -16,12 +16,12 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/registry"
|
||||
"github.com/goharbor/harbor/src/common/utils/registry/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/service/token"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// NewRepositoryClientForUI creates a repository client that can only be used to
|
||||
@ -32,8 +32,11 @@ func NewRepositoryClientForUI(username, repository string) (*registry.Repository
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uam := &auth.UserAgentModifier{
|
||||
UserAgent: "harbor-registry-client",
|
||||
}
|
||||
authorizer := auth.NewRawTokenAuthorizer(username, token.Registry)
|
||||
transport := registry.NewTransport(http.DefaultTransport, authorizer)
|
||||
transport := registry.NewTransport(http.DefaultTransport, authorizer, uam)
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
@ -48,3 +48,16 @@ type Repository struct {
|
||||
// OS of the image.
|
||||
Os string `json:"os,omitempty"`
|
||||
}
|
||||
|
||||
// Retag describes a retag request
|
||||
type Retag struct {
|
||||
|
||||
// The new tag
|
||||
Tag string `json:"tag"`
|
||||
|
||||
// Source images in format <project>/<repo>:<reference>
|
||||
SrcImage string `json:"src_image"`
|
||||
|
||||
// If target tag exists, whether override it
|
||||
Override bool `json:"override"`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user