From 03af3c5936b9943c18ae695e8043d3ff616832bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=BE=B7?= Date: Fri, 31 Aug 2018 18:14:32 +0800 Subject: [PATCH 1/6] Add image retag API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 陈德 --- src/common/models/retag.go | 47 +++++++++++++ src/common/models/retag_test.go | 65 ++++++++++++++++++ .../utils/registry/auth/tokenauthorizer.go | 2 +- src/common/utils/registry/repository.go | 25 +++++++ src/core/router.go | 1 + src/core/utils/utils.go | 4 +- src/ui/api/retag.go | 65 ++++++++++++++++++ src/ui/utils/retag.go | 68 +++++++++++++++++++ 8 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 src/common/models/retag.go create mode 100644 src/common/models/retag_test.go create mode 100644 src/ui/api/retag.go create mode 100644 src/ui/utils/retag.go diff --git a/src/common/models/retag.go b/src/common/models/retag.go new file mode 100644 index 000000000..e0b1ab134 --- /dev/null +++ b/src/common/models/retag.go @@ -0,0 +1,47 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// 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" +) + +type RetagRequest struct { + SrcImage string `json:"src_image"` + DestImage string `json:"dest_image"` +} + +type Image struct { + Project string + Repo string + Tag string +} + +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 +} \ No newline at end of file diff --git a/src/common/models/retag_test.go b/src/common/models/retag_test.go new file mode 100644 index 000000000..5728dd859 --- /dev/null +++ b/src/common/models/retag_test.go @@ -0,0 +1,65 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// 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 ( + "testing" + "reflect" + "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: "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("expect to parse %s fail, but not", c.Input) + } + } + } +} \ No newline at end of file diff --git a/src/common/utils/registry/auth/tokenauthorizer.go b/src/common/utils/registry/auth/tokenauthorizer.go index 7f2eb0506..f06f120cc 100644 --- a/src/common/utils/registry/auth/tokenauthorizer.go +++ b/src/common/utils/registry/auth/tokenauthorizer.go @@ -246,7 +246,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 } diff --git a/src/common/utils/registry/repository.go b/src/common/utils/registry/repository.go index 126d198ee..2f56d05dd 100644 --- a/src/common/utils/registry/repository.go +++ b/src/common/utils/registry/repository.go @@ -258,6 +258,27 @@ func (r *Repository) DeleteManifest(digest string) error { } } +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 +483,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) } diff --git a/src/core/router.go b/src/core/router.go index a12f49364..c19693c9a 100644 --- a/src/core/router.go +++ b/src/core/router.go @@ -58,6 +58,7 @@ func initRouters() { // API beego.Router("/api/ping", &api.SystemInfoAPI{}, "get:Ping") beego.Router("/api/search", &api.SearchAPI{}) + beego.Router("api/retag", &api.RetagAPI{}, "post:Retag") beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post") beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs") beego.Router("/api/projects/:id([0-9]+)/_deletable", &api.ProjectAPI{}, "get:Deletable") diff --git a/src/core/utils/utils.go b/src/core/utils/utils.go index 48eb030c6..03f8f5e5b 100644 --- a/src/core/utils/utils.go +++ b/src/core/utils/utils.go @@ -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 diff --git a/src/ui/api/retag.go b/src/ui/api/retag.go new file mode 100644 index 000000000..1169c6947 --- /dev/null +++ b/src/ui/api/retag.go @@ -0,0 +1,65 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// 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 api + +import ( + "fmt" + + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/ui/utils" +) + +// RetagAPI retag an image +type RetagAPI struct { + BaseController +} + +func (r *RetagAPI) Retag() { + if !r.SecurityCtx.IsAuthenticated() { + r.HandleUnauthorized() + return + } + + request := models.RetagRequest{} + r.DecodeJSONReq(&request) + + srcImage, err := models.ParseImage(request.SrcImage) + if err != nil { + r.HandleBadRequest(fmt.Sprintf("invalid src image string '%s', should in format '/:'", request.SrcImage)) + return + } + destImage, err := models.ParseImage(request.DestImage) + if err != nil { + r.HandleBadRequest(fmt.Sprintf("invalid dest image string '%s', should in format '/:'", request.DestImage)) + return + } + + if !r.SecurityCtx.HasReadPerm(srcImage.Project) { + log.Errorf("user has no read permission to project '%s'", srcImage.Project) + r.HandleUnauthorized() + } + + if !r.SecurityCtx.HasWritePerm(destImage.Project) { + log.Errorf("user has no write permission to project '%s'", destImage.Project) + r.HandleUnauthorized() + } + + if err = utils.Retag(srcImage, destImage); err != nil { + r.HandleInternalServerError(fmt.Sprintf("%v", err)) + } +} + + diff --git a/src/ui/utils/retag.go b/src/ui/utils/retag.go new file mode 100644 index 000000000..a9a1624d1 --- /dev/null +++ b/src/ui/utils/retag.go @@ -0,0 +1,68 @@ +package utils + +import ( + "fmt" + + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/registry" + "github.com/goharbor/harbor/src/common/utils/log" + + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/distribution/manifest/schema2" +) + +func Retag(srcImage, destImage *models.Image) error { + srcClient, err := NewRepositoryClientForUI("harbor-ui", getRepoName(srcImage)) + if err != nil { + return err + } + destClient := srcClient + if getRepoName(srcImage) != getRepoName(destImage) { + destClient, err = NewRepositoryClientForUI("harbor-ui", getRepoName(destImage)) + if err != nil { + return err + } + } + + 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 getRepoName(srcImage) != getRepoName(destImage) { + 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) +} \ No newline at end of file From 48d2435146e0fed5d84cc17872060ea02f83a85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=BE=B7?= Date: Sat, 1 Sep 2018 14:55:01 +0800 Subject: [PATCH 2/6] Fix notification event filtered because of user agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 陈德 --- src/common/dao/project.go | 6 ++++ .../utils/registry/auth/tokenauthorizer.go | 11 +++++++ src/core/utils/utils.go | 5 +++- src/ui/api/retag.go | 17 +++++++++-- src/ui/utils/retag.go | 29 +++++++++++++++++-- 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/common/dao/project.go b/src/common/dao/project.go index c39c3e79a..80751a35f 100644 --- a/src/common/dao/project.go +++ b/src/common/dao/project.go @@ -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) { diff --git a/src/common/utils/registry/auth/tokenauthorizer.go b/src/common/utils/registry/auth/tokenauthorizer.go index f06f120cc..1408ea11b 100644 --- a/src/common/utils/registry/auth/tokenauthorizer.go +++ b/src/common/utils/registry/auth/tokenauthorizer.go @@ -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 diff --git a/src/core/utils/utils.go b/src/core/utils/utils.go index 03f8f5e5b..5a07cf3b4 100644 --- a/src/core/utils/utils.go +++ b/src/core/utils/utils.go @@ -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, } diff --git a/src/ui/api/retag.go b/src/ui/api/retag.go index 1169c6947..b0259dc9b 100644 --- a/src/ui/api/retag.go +++ b/src/ui/api/retag.go @@ -17,6 +17,7 @@ package api import ( "fmt" + "github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/ui/utils" @@ -47,19 +48,31 @@ func (r *RetagAPI) Retag() { return } + if !dao.RepositoryExists(fmt.Sprintf("%s/%s", srcImage.Project, srcImage.Repo)) { + log.Errorf("source repository '%s/%s' not exist", srcImage.Project, srcImage.Repo) + r.HandleNotFound(fmt.Sprintf("repository '%s/%s' not found", srcImage.Project, srcImage.Repo)) + return + } + + if !dao.ProjectExistsByName(destImage.Project) { + log.Errorf("destination project '%s' not exist", destImage.Project) + r.HandleNotFound(fmt.Sprintf("project '%s' not found", destImage.Project)) + return + } + if !r.SecurityCtx.HasReadPerm(srcImage.Project) { log.Errorf("user has no read permission to project '%s'", srcImage.Project) r.HandleUnauthorized() + return } if !r.SecurityCtx.HasWritePerm(destImage.Project) { log.Errorf("user has no write permission to project '%s'", destImage.Project) r.HandleUnauthorized() + return } if err = utils.Retag(srcImage, destImage); err != nil { r.HandleInternalServerError(fmt.Sprintf("%v", err)) } } - - diff --git a/src/ui/utils/retag.go b/src/ui/utils/retag.go index a9a1624d1..1abf43bb2 100644 --- a/src/ui/utils/retag.go +++ b/src/ui/utils/retag.go @@ -1,11 +1,25 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// 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/registry" "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" @@ -24,6 +38,17 @@ func Retag(srcImage, destImage *models.Image) error { } } + _, 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 { @@ -65,4 +90,4 @@ func Retag(srcImage, destImage *models.Image) error { func getRepoName(image *models.Image) string { return fmt.Sprintf("%s/%s", image.Project, image.Repo) -} \ No newline at end of file +} From 75f1cdb449e2a3eb33383a21b22b373a88c67f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=BE=B7?= Date: Sat, 1 Sep 2018 15:10:28 +0800 Subject: [PATCH 3/6] Update swagger file to add retag API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 陈德 --- docs/swagger.yaml | 35 +++++++++++++++++++++++++ src/common/models/retag.go | 12 ++++++--- src/common/models/retag_test.go | 19 +++++++------- src/common/utils/registry/repository.go | 1 + src/ui/api/retag.go | 1 + src/ui/utils/retag.go | 6 +++-- 6 files changed, 59 insertions(+), 15 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 638ce5cbc..d9a2a75e9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -41,6 +41,32 @@ paths: $ref: '#/definitions/Search' '500': description: Unexpected internal errors. + /retag: + post: + summary: Retag an image + description: > + This endpoint tags an image with another tag in the same repo, or + to another repo or project. + parameters: + - name: request + in: body + description: reqeust to given source image and target image + 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. + '500': + description: Unexpected internal errors. /projects: get: summary: List projects @@ -2966,6 +2992,15 @@ definitions: type: array items: $ref: '#/definitions/SearchResult' + RetagReq: + type: object + properties: + src_image: + description: Source image to be retagged, e.g. 'stage/app:v1.0' + type: string + dest_image: + description: Destination image tag to, e.g. 'product/app:v1.0' + type: string SearchRepository: type: object properties: diff --git a/src/common/models/retag.go b/src/common/models/retag.go index e0b1ab134..5ab9763f1 100644 --- a/src/common/models/retag.go +++ b/src/common/models/retag.go @@ -19,17 +19,21 @@ import ( "strings" ) +// RetagRequest gives the source image and target image of retag type RetagRequest struct { - SrcImage string `json:"src_image"` - DestImage string `json:"dest_image"` + SrcImage string `json:"src_image"` + DestImage string `json:"dest_image"` } +// 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 { @@ -38,10 +42,10 @@ func ParseImage(image string) (*Image, error) { i := strings.SplitN(repo[1], ":", 2) res := &Image{ Project: repo[0], - Repo: i[0], + Repo: i[0], } if len(i) == 2 { res.Tag = i[1] } return res, nil -} \ No newline at end of file +} diff --git a/src/common/models/retag_test.go b/src/common/models/retag_test.go index 5728dd859..f30d82ae2 100644 --- a/src/common/models/retag_test.go +++ b/src/common/models/retag_test.go @@ -15,23 +15,24 @@ package models import ( - "testing" "reflect" + "testing" + "github.com/stretchr/testify/assert" ) func TestParseImage(t *testing.T) { cases := []struct { - Input string + Input string Expected *Image - Valid bool - } { + Valid bool + }{ { Input: "library/busybox", Expected: &Image{ Project: "library", - Repo: "busybox", - Tag: "", + Repo: "busybox", + Tag: "", }, Valid: true, }, @@ -39,8 +40,8 @@ func TestParseImage(t *testing.T) { Input: "library/busybox:v1.0", Expected: &Image{ Project: "library", - Repo: "busybox", - Tag: "v1.0", + Repo: "busybox", + Tag: "v1.0", }, Valid: true, }, @@ -62,4 +63,4 @@ func TestParseImage(t *testing.T) { } } } -} \ No newline at end of file +} diff --git a/src/common/utils/registry/repository.go b/src/common/utils/registry/repository.go index 2f56d05dd..7e7a8a27f 100644 --- a/src/common/utils/registry/repository.go +++ b/src/common/utils/registry/repository.go @@ -258,6 +258,7 @@ 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") diff --git a/src/ui/api/retag.go b/src/ui/api/retag.go index b0259dc9b..8334dda37 100644 --- a/src/ui/api/retag.go +++ b/src/ui/api/retag.go @@ -28,6 +28,7 @@ type RetagAPI struct { BaseController } +// Retag tags an image to another func (r *RetagAPI) Retag() { if !r.SecurityCtx.IsAuthenticated() { r.HandleUnauthorized() diff --git a/src/ui/utils/retag.go b/src/ui/utils/retag.go index 1abf43bb2..0394f0408 100644 --- a/src/ui/utils/retag.go +++ b/src/ui/utils/retag.go @@ -25,13 +25,15 @@ import ( "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 getRepoName(srcImage) != getRepoName(destImage) { + if !isSameRepo { destClient, err = NewRepositoryClientForUI("harbor-ui", getRepoName(destImage)) if err != nil { return err @@ -70,7 +72,7 @@ func Retag(srcImage, destImage *models.Image) error { return nil } - if getRepoName(srcImage) != getRepoName(destImage) { + if !isSameRepo { for _, descriptor := range manifest.References() { err := destClient.MountBlob(descriptor.Digest.String(), srcClient.Name) if err != nil { From 03d5157eafb69efaa10c399831805e4ccdd18be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=BE=B7?= Date: Tue, 18 Sep 2018 14:05:35 +0800 Subject: [PATCH 4/6] Updae retag api spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 陈德 --- docs/swagger.yaml | 62 ++++++++++++++------------ src/common/models/retag.go | 5 ++- src/common/models/retag_test.go | 9 ++++ src/core/api/repository.go | 76 +++++++++++++++++++++++++++++++ src/core/router.go | 3 +- src/ui/api/retag.go | 79 --------------------------------- 6 files changed, 122 insertions(+), 112 deletions(-) delete mode 100644 src/ui/api/retag.go diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d9a2a75e9..0ca3f5c8c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -41,32 +41,6 @@ paths: $ref: '#/definitions/Search' '500': description: Unexpected internal errors. - /retag: - post: - summary: Retag an image - description: > - This endpoint tags an image with another tag in the same repo, or - to another repo or project. - parameters: - - name: request - in: body - description: reqeust to given source image and target image - 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. - '500': - description: Unexpected internal errors. /projects: get: summary: List projects @@ -1157,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: reqeust to given 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. @@ -2995,12 +2996,15 @@ definitions: 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 - dest_image: - description: Destination image tag to, e.g. 'product/app:v1.0' - type: string + override: + description: If target tag already exists, whether to override it + type: boolean SearchRepository: type: object properties: diff --git a/src/common/models/retag.go b/src/common/models/retag.go index 5ab9763f1..7f2951435 100644 --- a/src/common/models/retag.go +++ b/src/common/models/retag.go @@ -21,8 +21,9 @@ import ( // RetagRequest gives the source image and target image of retag type RetagRequest struct { - SrcImage string `json:"src_image"` - DestImage string `json:"dest_image"` + Tag string `json:"tag"` // The new tag + SrcImage string `json:"src_image"` // Source images in format /: + Override bool `json:"override"` // If target tag exists, whether override it } // Image holds each part (project, repo, tag) of an image name diff --git a/src/common/models/retag_test.go b/src/common/models/retag_test.go index f30d82ae2..721d0a340 100644 --- a/src/common/models/retag_test.go +++ b/src/common/models/retag_test.go @@ -45,6 +45,15 @@ func TestParseImage(t *testing.T) { }, Valid: true, }, + { + Input: "library/busybox:sha256:9e2c9d5f44efbb6ee83aecd17a120c513047d289d142ec5738c9f02f9b24ad07", + Expected: &Image{ + Project: "library", + Repo: "busybox", + Tag: "sha256:9e2c9d5f44efbb6ee83aecd17a120c513047d289d142ec5738c9f02f9b24ad07", + }, + Valid: true, + }, { Input: "busybox/v1.0", Valid: false, diff --git a/src/core/api/repository.go b/src/core/api/repository.go index 6f1b46d3b..780e4d9bf 100644 --- a/src/core/api/repository.go +++ b/src/core/api/repository.go @@ -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 '/:'", 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 = uiutils.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") diff --git a/src/core/router.go b/src/core/router.go index c19693c9a..78734f449 100644 --- a/src/core/router.go +++ b/src/core/router.go @@ -58,7 +58,6 @@ func initRouters() { // API beego.Router("/api/ping", &api.SystemInfoAPI{}, "get:Ping") beego.Router("/api/search", &api.SearchAPI{}) - beego.Router("api/retag", &api.RetagAPI{}, "post:Retag") beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post") beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs") beego.Router("/api/projects/:id([0-9]+)/_deletable", &api.ProjectAPI{}, "get:Deletable") @@ -73,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") diff --git a/src/ui/api/retag.go b/src/ui/api/retag.go deleted file mode 100644 index 8334dda37..000000000 --- a/src/ui/api/retag.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2017 VMware, Inc. All Rights Reserved. -// -// 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 api - -import ( - "fmt" - - "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/ui/utils" -) - -// RetagAPI retag an image -type RetagAPI struct { - BaseController -} - -// Retag tags an image to another -func (r *RetagAPI) Retag() { - if !r.SecurityCtx.IsAuthenticated() { - r.HandleUnauthorized() - return - } - - request := models.RetagRequest{} - r.DecodeJSONReq(&request) - - srcImage, err := models.ParseImage(request.SrcImage) - if err != nil { - r.HandleBadRequest(fmt.Sprintf("invalid src image string '%s', should in format '/:'", request.SrcImage)) - return - } - destImage, err := models.ParseImage(request.DestImage) - if err != nil { - r.HandleBadRequest(fmt.Sprintf("invalid dest image string '%s', should in format '/:'", request.DestImage)) - return - } - - if !dao.RepositoryExists(fmt.Sprintf("%s/%s", srcImage.Project, srcImage.Repo)) { - log.Errorf("source repository '%s/%s' not exist", srcImage.Project, srcImage.Repo) - r.HandleNotFound(fmt.Sprintf("repository '%s/%s' not found", srcImage.Project, srcImage.Repo)) - return - } - - if !dao.ProjectExistsByName(destImage.Project) { - log.Errorf("destination project '%s' not exist", destImage.Project) - r.HandleNotFound(fmt.Sprintf("project '%s' not found", destImage.Project)) - return - } - - if !r.SecurityCtx.HasReadPerm(srcImage.Project) { - log.Errorf("user has no read permission to project '%s'", srcImage.Project) - r.HandleUnauthorized() - return - } - - if !r.SecurityCtx.HasWritePerm(destImage.Project) { - log.Errorf("user has no write permission to project '%s'", destImage.Project) - r.HandleUnauthorized() - return - } - - if err = utils.Retag(srcImage, destImage); err != nil { - r.HandleInternalServerError(fmt.Sprintf("%v", err)) - } -} From b648084d95ef2fa691df53d47dcf4db376901ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=BE=B7?= Date: Mon, 8 Oct 2018 19:23:45 +0800 Subject: [PATCH 5/6] Improve code styles and fix after Harbor refactoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 陈德 --- docs/swagger.yaml | 4 ++-- src/common/models/retag.go | 4 ++-- src/common/models/retag_test.go | 4 ++-- src/common/utils/registry/auth/tokenauthorizer.go | 2 +- src/core/api/repository.go | 2 +- src/{ui => core}/utils/retag.go | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) rename src/{ui => core}/utils/retag.go (98%) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 0ca3f5c8c..795bcf2f8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1139,7 +1139,7 @@ paths: parameters: - name: request in: body - description: reqeust to given source image and target tag + description: Request to give source image and target tag. required: true schema: $ref: '#/definitions/RetagReq' @@ -1149,7 +1149,7 @@ paths: '200': description: Image retag successfully. '400': - description: Invalid image values provided + description: Invalid image values provided. '401': description: User has no permission to the source project or destination project. '404': diff --git a/src/common/models/retag.go b/src/common/models/retag.go index 7f2951435..d684922b8 100644 --- a/src/common/models/retag.go +++ b/src/common/models/retag.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// 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. @@ -38,7 +38,7 @@ type Image struct { 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) + return nil, fmt.Errorf("Unable to parse image from string: %s", image) } i := strings.SplitN(repo[1], ":", 2) res := &Image{ diff --git a/src/common/models/retag_test.go b/src/common/models/retag_test.go index 721d0a340..cf80a80d0 100644 --- a/src/common/models/retag_test.go +++ b/src/common/models/retag_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// 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. @@ -68,7 +68,7 @@ func TestParseImage(t *testing.T) { } } else { if err != nil { - t.Errorf("expect to parse %s fail, but not", c.Input) + t.Errorf("failed to parse image %s", c.Input) } } } diff --git a/src/common/utils/registry/auth/tokenauthorizer.go b/src/common/utils/registry/auth/tokenauthorizer.go index 1408ea11b..ccb4cca49 100644 --- a/src/common/utils/registry/auth/tokenauthorizer.go +++ b/src/common/utils/registry/auth/tokenauthorizer.go @@ -257,7 +257,7 @@ func ping(client *http.Client, endpoint string) (string, string, error) { } } - log.Warningf("schemas %v are unsupported", challenges) + log.Warningf("Schemas %v are unsupported", challenges) return "", "", nil } diff --git a/src/core/api/repository.go b/src/core/api/repository.go index 780e4d9bf..0802640be 100644 --- a/src/core/api/repository.go +++ b/src/core/api/repository.go @@ -491,7 +491,7 @@ func (ra *RepositoryAPI) Retag() { } // Retag the image - if err = uiutils.Retag(srcImage, &models.Image{ + if err = coreutils.Retag(srcImage, &models.Image{ Project: project, Repo: repo, Tag: request.Tag, diff --git a/src/ui/utils/retag.go b/src/core/utils/retag.go similarity index 98% rename from src/ui/utils/retag.go rename to src/core/utils/retag.go index 0394f0408..b53f5b713 100644 --- a/src/ui/utils/retag.go +++ b/src/core/utils/retag.go @@ -1,4 +1,4 @@ -// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// 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. From a1b4729aa75471027f00fcb7618ac6359f25fec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=BE=B7?= Date: Wed, 17 Oct 2018 16:34:01 +0800 Subject: [PATCH 6/6] Add more unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 陈德 --- src/common/dao/project_test.go | 27 ++++++ .../registry/auth/tokenauthorizer_test.go | 14 +++ src/common/utils/registry/repository_test.go | 34 +++++++ src/core/api/harborapi_test.go | 15 ++- src/core/api/repository_test.go | 94 +++++++++++++++++++ tests/apitests/apilib/repository.go | 13 +++ 6 files changed, 196 insertions(+), 1 deletion(-) diff --git a/src/common/dao/project_test.go b/src/common/dao/project_test.go index 9b0f0abdb..7358840b9 100644 --- a/src/common/dao/project_test.go +++ b/src/common/dao/project_test.go @@ -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) + } +} diff --git a/src/common/utils/registry/auth/tokenauthorizer_test.go b/src/common/utils/registry/auth/tokenauthorizer_test.go index fc7d2ec90..b2343f0ff 100644 --- a/src/common/utils/registry/auth/tokenauthorizer_test.go +++ b/src/common/utils/registry/auth/tokenauthorizer_test.go @@ -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) + } +} diff --git a/src/common/utils/registry/repository_test.go b/src/common/utils/registry/repository_test.go index 8293920e6..86efa96cd 100644 --- a/src/common/utils/registry/repository_test.go +++ b/src/common/utils/registry/repository_test.go @@ -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) + } +} diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index 4dace6345..e830ae7e8 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -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") @@ -617,6 +617,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) diff --git a/src/core/api/repository_test.go b/src/core/api/repository_test.go index 592d9b4a5..bf61799e8 100644 --- a/src/core/api/repository_test.go +++ b/src/core/api/repository_test.go @@ -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") +} diff --git a/tests/apitests/apilib/repository.go b/tests/apitests/apilib/repository.go index 8d4b9e756..d3943e118 100644 --- a/tests/apitests/apilib/repository.go +++ b/tests/apitests/apilib/repository.go @@ -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 /: + SrcImage string `json:"src_image"` + + // If target tag exists, whether override it + Override bool `json:"override"` +}