Add image retag API

Signed-off-by: 陈德 <chende@caicloud.io>
This commit is contained in:
陈德 2018-08-31 18:14:32 +08:00
parent 002727d850
commit 03af3c5936
8 changed files with 274 additions and 3 deletions

View File

@ -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
}

View File

@ -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)
}
}
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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")

View File

@ -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

65
src/ui/api/retag.go Normal file
View File

@ -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 '<project>/<repo>:<tag>'", request.SrcImage))
return
}
destImage, err := models.ParseImage(request.DestImage)
if err != nil {
r.HandleBadRequest(fmt.Sprintf("invalid dest image string '%s', should in format '<project>/<repo>:<tag>'", 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))
}
}

68
src/ui/utils/retag.go Normal file
View File

@ -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)
}