Merge pull request #113 from ywk253100/delete_image

API for deleting repository and tag
This commit is contained in:
Daniel Jiang 2016-04-19 13:58:24 +08:00
commit 6779274b54
19 changed files with 921 additions and 160 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ Deploy/config/ui/env
Deploy/config/ui/app.conf
Deploy/config/db/env
Deploy/harbor.cfg
ui/ui

View File

@ -11,6 +11,8 @@ storage:
maintenance:
uploadpurging:
enabled: false
delete:
enabled: true
http:
addr: :5000
secret: placeholder

View File

@ -57,7 +57,10 @@ func (b *BaseAPI) ValidateUser() int {
username, password, ok := b.Ctx.Request.BasicAuth()
if ok {
log.Infof("Requst with Basic Authentication header, username: %s", username)
user, err := auth.Login(models.AuthModel{username, password})
user, err := auth.Login(models.AuthModel{
Principal: username,
Password: password,
})
if err != nil {
log.Errorf("Error while trying to login, username: %s, error: %v", username, err)
user = nil

View File

@ -18,6 +18,7 @@ package api
import (
"encoding/json"
"net/http"
"os"
"strconv"
"strings"
"time"
@ -26,6 +27,9 @@ import (
"github.com/vmware/harbor/models"
svc_utils "github.com/vmware/harbor/service/utils"
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/vmware/harbor/utils/registry/auth"
"github.com/vmware/harbor/utils/registry/errors"
)
// RepositoryAPI handles request to /api/repositories /api/repositories/tags /api/repositories/manifests, the parm has to be put
@ -36,6 +40,7 @@ type RepositoryAPI struct {
BaseAPI
userID int
username string
registry *registry.Registry
}
// Prepare will set a non existent user ID in case the request tries to view repositories under a project he doesn't has permission.
@ -53,6 +58,43 @@ func (ra *RepositoryAPI) Prepare() {
} else {
ra.username = username
}
var client *http.Client
//no session, initialize a standard auth handler
if ra.userID == dao.NonExistUserID && len(ra.username) == 0 {
username, password, _ := ra.Ctx.Request.BasicAuth()
credential := auth.NewBasicAuthCredential(username, password)
client = registry.NewClientStandardAuthHandlerEmbeded(credential)
log.Debug("initializing standard auth handler")
} else {
// session works, initialize a username auth handler
username := ra.username
if len(username) == 0 {
user, err := dao.GetUser(models.User{
UserID: ra.userID,
})
if err != nil {
log.Errorf("error occurred whiling geting user for initializing a username auth handler: %v", err)
return
}
username = user.Username
}
client = registry.NewClientUsernameAuthHandlerEmbeded(username)
log.Debug("initializing username auth handler: %s", username)
}
endpoint := os.Getenv("REGISTRY_URL")
r, err := registry.New(endpoint, client)
if err != nil {
log.Fatalf("error occurred while initializing auth handler for repository API: %v", err)
}
ra.registry = r
}
// Get ...
@ -77,11 +119,13 @@ func (ra *RepositoryAPI) Get() {
ra.RenderError(http.StatusForbidden, "")
return
}
repoList, err := svc_utils.GetRepoFromCache()
if err != nil {
log.Errorf("Failed to get repo from cache, error: %v", err)
ra.RenderError(http.StatusInternalServerError, "internal sever error")
}
projectName := p.Name
q := ra.GetString("q")
var resp []string
@ -105,6 +149,56 @@ func (ra *RepositoryAPI) Get() {
ra.ServeJSON()
}
// Delete ...
func (ra *RepositoryAPI) Delete() {
repoName := ra.GetString("repo_name")
if len(repoName) == 0 {
ra.CustomAbort(http.StatusBadRequest, "repo_name is nil")
}
tags := []string{}
tag := ra.GetString("tag")
if len(tag) == 0 {
tagList, err := ra.registry.ListTag(repoName)
if err != nil {
e, ok := errors.ParseError(err)
if ok {
log.Info(e)
ra.CustomAbort(e.StatusCode, e.Message)
} else {
log.Error(err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
}
}
tags = append(tags, tagList...)
} else {
tags = append(tags, tag)
}
for _, t := range tags {
if err := ra.registry.DeleteTag(repoName, t); err != nil {
e, ok := errors.ParseError(err)
if ok {
ra.CustomAbort(e.StatusCode, e.Message)
} else {
log.Error(err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
}
}
log.Infof("delete tag: %s %s", repoName, t)
}
go func() {
log.Debug("refreshing catalog cache")
if err := svc_utils.RefreshCatalogCache(); err != nil {
log.Errorf("error occurred while refresh catalog cache: %v", err)
}
}()
}
type tag struct {
Name string `json:"name"`
Tags []string `json:"tags"`
@ -128,15 +222,19 @@ func (ra *RepositoryAPI) GetTags() {
var tags []string
repoName := ra.GetString("repo_name")
result, err := svc_utils.RegistryAPIGet(svc_utils.BuildRegistryURL(repoName, "tags", "list"), ra.username)
tags, err := ra.registry.ListTag(repoName)
if err != nil {
log.Errorf("Failed to get repo tags, repo name: %s, error: %v", repoName, err)
ra.RenderError(http.StatusInternalServerError, "Failed to get repo tags")
} else {
t := tag{}
json.Unmarshal(result, &t)
tags = t.Tags
e, ok := errors.ParseError(err)
if ok {
log.Info(e)
ra.CustomAbort(e.StatusCode, e.Message)
} else {
log.Error(err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
}
}
ra.Data["json"] = tags
ra.ServeJSON()
}
@ -148,14 +246,20 @@ func (ra *RepositoryAPI) GetManifests() {
item := models.RepoItem{}
result, err := svc_utils.RegistryAPIGet(svc_utils.BuildRegistryURL(repoName, "manifests", tag), ra.username)
_, _, payload, err := ra.registry.PullManifest(repoName, tag, registry.ManifestVersion1)
if err != nil {
log.Errorf("Failed to get manifests for repo, repo name: %s, tag: %s, error: %v", repoName, tag, err)
ra.RenderError(http.StatusInternalServerError, "Internal Server Error")
return
e, ok := errors.ParseError(err)
if ok {
log.Info(e)
ra.CustomAbort(e.StatusCode, e.Message)
} else {
log.Error(err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
}
}
mani := manifest{}
err = json.Unmarshal(result, &mani)
err = json.Unmarshal(payload, &mani)
if err != nil {
log.Errorf("Failed to decode json from response for manifests, repo name: %s, tag: %s, error: %v", repoName, tag, err)
ra.RenderError(http.StatusInternalServerError, "Internal Server Error")

View File

@ -49,7 +49,10 @@ func (c *CommonController) Login() {
principal := c.GetString("principal")
password := c.GetString("password")
user, err := auth.Login(models.AuthModel{principal, password})
user, err := auth.Login(models.AuthModel{
Principal: principal,
Password: password,
})
if err != nil {
log.Errorf("Error occurred in UserLogin: %v", err)
c.CustomAbort(http.StatusUnauthorized, "")

View File

@ -206,7 +206,10 @@ func TestLoginByUserName(t *testing.T) {
Password: "Abc12345",
}
loginUser, err := LoginByDb(models.AuthModel{userQuery.Username, userQuery.Password})
loginUser, err := LoginByDb(models.AuthModel{
Principal: userQuery.Username,
Password: userQuery.Password,
})
if err != nil {
t.Errorf("Error occurred in LoginByDb: %v", err)
}
@ -226,7 +229,10 @@ func TestLoginByEmail(t *testing.T) {
Password: "Abc12345",
}
loginUser, err := LoginByDb(models.AuthModel{userQuery.Email, userQuery.Password})
loginUser, err := LoginByDb(models.AuthModel{
Principal: userQuery.Email,
Password: userQuery.Password,
})
if err != nil {
t.Errorf("Error occurred in LoginByDb: %v", err)
}

View File

@ -58,6 +58,10 @@ func IsAdminRole(userIDOrUsername interface{}) (bool, error) {
return false, fmt.Errorf("invalid parameter, only int and string are supported: %v", userIDOrUsername)
}
if u.UserID == NonExistUserID && len(u.Username) == 0 {
return false, nil
}
user, err := GetUser(u)
if err != nil {
return false, err

View File

@ -38,7 +38,7 @@ const manifestPattern = `^application/vnd.docker.distribution.manifest.v\d\+json
// Post handles POST request, and records audit log or refreshes cache based on event.
func (n *NotificationHandler) Post() {
var notification models.Notification
// log.Info("Notification Handler triggered!\n")
//log.Info("Notification Handler triggered!\n")
// log.Infof("request body in string: %s", string(n.Ctx.Input.CopyBody()))
err := json.Unmarshal(n.Ctx.Input.CopyBody(1<<32), &notification)

View File

@ -13,7 +13,7 @@
limitations under the License.
*/
package utils
package token
import (
"crypto"
@ -80,7 +80,7 @@ func FilterAccess(username string, authenticated bool, a *token.ResourceActions)
return
}
if exist {
permission = "RW"
permission = "RWM"
} else {
permission = ""
log.Infof("project %s does not exist, set empty permission for admin\n", projectName)
@ -96,6 +96,9 @@ func FilterAccess(username string, authenticated bool, a *token.ResourceActions)
if strings.Contains(permission, "W") {
a.Actions = append(a.Actions, "push")
}
if strings.Contains(permission, "M") {
a.Actions = append(a.Actions, "*")
}
if strings.Contains(permission, "R") || dao.IsProjectPublic(projectName) {
a.Actions = append(a.Actions, "pull")
}

View File

@ -13,53 +13,53 @@
limitations under the License.
*/
package service
package token
import (
"net/http"
"github.com/vmware/harbor/auth"
"github.com/vmware/harbor/models"
svc_utils "github.com/vmware/harbor/service/utils"
//svc_utils "github.com/vmware/harbor/service/utils"
"github.com/vmware/harbor/utils/log"
"github.com/astaxie/beego"
"github.com/docker/distribution/registry/auth/token"
)
// TokenHandler handles request on /service/token, which is the auth provider for registry.
type TokenHandler struct {
// Handler handles request on /service/token, which is the auth provider for registry.
type Handler struct {
beego.Controller
}
// Get handles GET request, it checks the http header for user credentials
// and parse service and scope based on docker registry v2 standard,
// checkes the permission agains local DB and generates jwt token.
func (a *TokenHandler) Get() {
func (h *Handler) Get() {
request := a.Ctx.Request
request := h.Ctx.Request
log.Infof("request url: %v", request.URL.String())
username, password, _ := request.BasicAuth()
authenticated := authenticate(username, password)
service := a.GetString("service")
scopes := a.GetStrings("scope")
service := h.GetString("service")
scopes := h.GetStrings("scope")
log.Debugf("scopes: %+v", scopes)
if len(scopes) == 0 && !authenticated {
log.Info("login request with invalid credentials")
a.CustomAbort(http.StatusUnauthorized, "")
h.CustomAbort(http.StatusUnauthorized, "")
}
access := svc_utils.GetResourceActions(scopes)
access := GetResourceActions(scopes)
for _, a := range access {
svc_utils.FilterAccess(username, authenticated, a)
FilterAccess(username, authenticated, a)
}
a.serveToken(username, service, access)
h.serveToken(username, service, access)
}
func (a *TokenHandler) serveToken(username, service string, access []*token.ResourceActions) {
writer := a.Ctx.ResponseWriter
func (h *Handler) serveToken(username, service string, access []*token.ResourceActions) {
writer := h.Ctx.ResponseWriter
//create token
rawToken, err := svc_utils.MakeToken(username, service, access)
rawToken, err := MakeToken(username, service, access)
if err != nil {
log.Errorf("Failed to make token, error: %v", err)
writer.WriteHeader(http.StatusInternalServerError)
@ -67,12 +67,15 @@ func (a *TokenHandler) serveToken(username, service string, access []*token.Reso
}
tk := make(map[string]string)
tk["token"] = rawToken
a.Data["json"] = tk
a.ServeJSON()
h.Data["json"] = tk
h.ServeJSON()
}
func authenticate(principal, password string) bool {
user, err := auth.Login(models.AuthModel{principal, password})
user, err := auth.Login(models.AuthModel{
Principal: principal,
Password: password,
})
if err != nil {
log.Errorf("Error occurred in UserLogin: %v", err)
return false

View File

@ -16,11 +16,11 @@
package utils
import (
"encoding/json"
"os"
"time"
"github.com/vmware/harbor/models"
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/astaxie/beego/cache"
)
@ -28,6 +28,8 @@ import (
// Cache is the global cache in system.
var Cache cache.Cache
var registryClient *registry.Registry
const catalogKey string = "catalog"
func init() {
@ -36,20 +38,39 @@ func init() {
if err != nil {
log.Errorf("Failed to initialize cache, error:%v", err)
}
endpoint := os.Getenv("REGISTRY_URL")
client := registry.NewClientUsernameAuthHandlerEmbeded("admin")
registryClient, err = registry.New(endpoint, client)
if err != nil {
log.Fatalf("error occurred while initializing authentication handler used by cache: %v", err)
}
}
// RefreshCatalogCache calls registry's API to get repository list and write it to cache.
func RefreshCatalogCache() error {
result, err := RegistryAPIGet(BuildRegistryURL("_catalog"), "")
log.Debug("refreshing catalog cache...")
rs, err := registryClient.Catalog()
if err != nil {
return err
}
repoResp := models.Repo{}
err = json.Unmarshal(result, &repoResp)
if err != nil {
return err
repos := []string{}
for _, repo := range rs {
tags, err := registryClient.ListTag(repo)
if err != nil {
log.Errorf("error occurred while list tag for %s: %v", repo, err)
return err
}
if len(tags) != 0 {
repos = append(repos, repo)
log.Debugf("add %s to catalog cache", repo)
}
}
Cache.Put(catalogKey, repoResp.Repositories, 600*time.Second)
Cache.Put(catalogKey, repos, 600*time.Second)
return nil
}

View File

@ -1,114 +0,0 @@
/*
Copyright (c) 2016 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 (
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"github.com/vmware/harbor/utils/log"
)
// BuildRegistryURL ...
func BuildRegistryURL(segments ...string) string {
registryURL := os.Getenv("REGISTRY_URL")
if registryURL == "" {
registryURL = "http://localhost:5000"
}
url := registryURL + "/v2"
for _, s := range segments {
if s == "v2" {
log.Debugf("unnecessary v2 in %v", segments)
continue
}
url += "/" + s
}
return url
}
// RegistryAPIGet triggers GET request to the URL which is the endpoint of registry and returns the response body.
// It will attach a valid jwt token to the request if registry requires.
func RegistryAPIGet(url, username string) ([]byte, error) {
log.Debugf("Registry API url: %s", url)
response, err := http.Get(url)
if err != nil {
return nil, err
}
result, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusOK {
return result, nil
} else if response.StatusCode == http.StatusUnauthorized {
authenticate := response.Header.Get("WWW-Authenticate")
log.Debugf("authenticate header: %s", authenticate)
var service string
var scopes []string
//Disregard the case for hanlding multiple scopes for http call initiated from UI, as there's refactor planned.
re := regexp.MustCompile(`service=\"(.*?)\".*scope=\"(.*?)\"`)
res := re.FindStringSubmatch(authenticate)
if len(res) > 2 {
service = res[1]
scopes = append(scopes, res[2])
}
token, err := GenTokenForUI(username, service, scopes)
if err != nil {
return nil, err
}
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
request.Header.Add("Authorization", "Bearer "+token)
client := &http.Client{}
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
// log.Infof("via length: %d\n", len(via))
if len(via) >= 10 {
return fmt.Errorf("too many redirects")
}
for k, v := range via[0].Header {
if _, ok := req.Header[k]; !ok {
req.Header[k] = v
}
}
return nil
}
response, err = client.Do(request)
if err != nil {
return nil, err
}
if response.StatusCode != http.StatusOK {
errMsg := fmt.Sprintf("Unexpected return code from registry: %d", response.StatusCode)
log.Error(errMsg)
return nil, fmt.Errorf(errMsg)
}
result, err = ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
defer response.Body.Close()
return result, nil
} else {
return nil, errors.New(string(result))
}
}

View File

@ -19,6 +19,7 @@ import (
"github.com/vmware/harbor/api"
"github.com/vmware/harbor/controllers"
"github.com/vmware/harbor/service"
"github.com/vmware/harbor/service/token"
"github.com/astaxie/beego"
)
@ -63,5 +64,5 @@ func initRouters() {
//external service that hosted on harbor process:
beego.Router("/service/notifications", &service.NotificationHandler{})
beego.Router("/service/token", &service.TokenHandler{})
beego.Router("/service/token", &token.Handler{})
}

View File

@ -0,0 +1,32 @@
/*
Copyright (c) 2016 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 auth
import (
"net/http"
au "github.com/docker/distribution/registry/client/auth"
"github.com/vmware/harbor/utils/log"
)
// ParseChallengeFromResponse ...
func ParseChallengeFromResponse(resp *http.Response) []au.Challenge {
challenges := au.ResponseChallenges(resp)
log.Debugf("challenges: %v", challenges)
return challenges
}

View File

@ -0,0 +1,197 @@
/*
Copyright (c) 2016 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 auth
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
token_util "github.com/vmware/harbor/service/token"
"github.com/vmware/harbor/utils/log"
registry_errors "github.com/vmware/harbor/utils/registry/errors"
)
const (
// credential type
basicAuth string = "basic_auth"
secretKey string = "secret_key"
)
// Handler authorizes the request when encounters a 401 error
type Handler interface {
// Schema : basic, bearer
Schema() string
//AuthorizeRequest adds basic auth or token auth to the header of request
AuthorizeRequest(req *http.Request, params map[string]string) error
}
// Credential ...
type Credential interface {
// AddAuthorization adds authorization information to request
AddAuthorization(req *http.Request)
}
type basicAuthCredential struct {
username string
password string
}
// NewBasicAuthCredential ...
func NewBasicAuthCredential(username, password string) Credential {
return &basicAuthCredential{
username: username,
password: password,
}
}
func (b *basicAuthCredential) AddAuthorization(req *http.Request) {
req.SetBasicAuth(b.username, b.password)
}
type token struct {
Token string `json:"token"`
}
type standardTokenHandler struct {
client *http.Client
credential Credential
}
// NewStandardTokenHandler returns a standard token handler. The handler will request a token
// from token server whose URL is specified in the "WWW-authentication" header and add it to
// the origin request
// TODO deal with https
func NewStandardTokenHandler(credential Credential) Handler {
return &standardTokenHandler{
client: &http.Client{
Transport: http.DefaultTransport,
},
credential: credential,
}
}
// Schema implements the corresponding method in interface AuthHandler
func (t *standardTokenHandler) Schema() string {
return "bearer"
}
// AuthorizeRequest implements the corresponding method in interface AuthHandler
func (t *standardTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
realm, ok := params["realm"]
if !ok {
return errors.New("no realm")
}
service := params["service"]
scope := params["scope"]
u, err := url.Parse(realm)
if err != nil {
return err
}
q := u.Query()
q.Add("service", service)
for _, s := range strings.Split(scope, " ") {
q.Add("scope", s)
}
u.RawQuery = q.Encode()
r, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return err
}
t.credential.AddAuthorization(r)
resp, err := t.client.Do(r)
if err != nil {
return err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return registry_errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
decoder := json.NewDecoder(resp.Body)
tk := &token{}
if err = decoder.Decode(tk); err != nil {
return err
}
req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", tk.Token))
log.Debugf("standardTokenHandler generated token successfully | %s %s", req.Method, req.URL)
return nil
}
type usernameTokenHandler struct {
username string
}
// NewUsernameTokenHandler returns a handler which will generate
// a token according the user's privileges
func NewUsernameTokenHandler(username string) Handler {
return &usernameTokenHandler{
username: username,
}
}
// Schema implements the corresponding method in interface AuthHandler
func (u *usernameTokenHandler) Schema() string {
return "bearer"
}
// AuthorizeRequest implements the corresponding method in interface AuthHandler
func (u *usernameTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
service := params["service"]
scopes := []string{}
scope := params["scope"]
if len(scope) != 0 {
scopes = strings.Split(scope, " ")
}
token, err := token_util.GenTokenForUI(u.username, service, scopes)
if err != nil {
return err
}
req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token))
log.Debugf("usernameTokenHandler generated token successfully | %s %s", req.Method, req.URL)
return nil
}

View File

@ -0,0 +1,38 @@
/*
Copyright (c) 2016 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 errors
import (
"fmt"
)
// Error : if response's status code is not 200 or does not meet requirement,
// an Error instance will be returned
type Error struct {
StatusCode int
Message string
}
// Error ...
func (e Error) Error() string {
return fmt.Sprintf("%d %s", e.StatusCode, e.Message)
}
// ParseError parses err, if err is type Error, convert it to Error
func ParseError(err error) (Error, bool) {
e, ok := err.(Error)
return e, ok
}

View File

@ -0,0 +1,116 @@
/*
Copyright (c) 2016 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 registry
import (
"net/http"
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry/auth"
)
// NewClient returns a http.Client according to the handlers provided
func NewClient(handlers []auth.Handler) *http.Client {
transport := NewAuthTransport(http.DefaultTransport, handlers)
return &http.Client{
Transport: transport,
}
}
// NewClientStandardAuthHandlerEmbeded return a http.Client which will authorize the request
// according to the credential provided and send it again when encounters a 401 error
func NewClientStandardAuthHandlerEmbeded(credential auth.Credential) *http.Client {
handlers := []auth.Handler{}
tokenHandler := auth.NewStandardTokenHandler(credential)
handlers = append(handlers, tokenHandler)
return NewClient(handlers)
}
// NewClientUsernameAuthHandlerEmbeded return a http.Client which will authorize the request
// according to the user's privileges and send it again when encounters a 401 error
func NewClientUsernameAuthHandlerEmbeded(username string) *http.Client {
handlers := []auth.Handler{}
tokenHandler := auth.NewUsernameTokenHandler(username)
handlers = append(handlers, tokenHandler)
return NewClient(handlers)
}
type authTransport struct {
transport http.RoundTripper
handlers []auth.Handler
}
// NewAuthTransport wraps the AuthHandlers to be http.RounTripper
func NewAuthTransport(transport http.RoundTripper, handlers []auth.Handler) http.RoundTripper {
return &authTransport{
transport: transport,
handlers: handlers,
}
}
// RoundTrip ...
func (a *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
originResp, originErr := a.transport.RoundTrip(req)
if originErr != nil {
return originResp, originErr
}
log.Debugf("%d | %s %s", originResp.StatusCode, req.Method, req.URL)
if originResp.StatusCode != http.StatusUnauthorized {
return originResp, nil
}
challenges := auth.ParseChallengeFromResponse(originResp)
reqChanged := false
for _, challenge := range challenges {
scheme := challenge.Scheme
for _, handler := range a.handlers {
if scheme != handler.Schema() {
log.Debugf("scheme not match: %s %s, skip", scheme, handler.Schema())
continue
}
if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil {
return nil, err
}
reqChanged = true
}
}
if !reqChanged {
log.Warning("no handler match scheme")
return originResp, nil
}
resp, err := a.transport.RoundTrip(req)
if err == nil {
log.Debugf("%d | %s %s", resp.StatusCode, req.Method, req.URL)
}
return resp, err
}

View File

@ -0,0 +1,25 @@
/*
Copyright (c) 2016 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 registry
import (
"github.com/docker/distribution"
)
// UnMarshal converts []byte to be distribution.Manifest
func UnMarshal(mediaType string, data []byte) (distribution.Manifest, distribution.Descriptor, error) {
return distribution.UnmarshalManifest(mediaType, data)
}

316
utils/registry/registry.go Normal file
View File

@ -0,0 +1,316 @@
/*
Copyright (c) 2016 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 registry
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/vmware/harbor/utils/registry/errors"
)
// Registry holds information of a registry entiry
type Registry struct {
Endpoint *url.URL
client *http.Client
ub *uRLBuilder
}
type uRLBuilder struct {
root *url.URL
}
var (
// ManifestVersion1 : schema 1
ManifestVersion1 = manifest.Versioned{
SchemaVersion: 1,
MediaType: schema1.MediaTypeManifest,
}
// ManifestVersion2 : schema 2
ManifestVersion2 = manifest.Versioned{
SchemaVersion: 2,
MediaType: schema2.MediaTypeManifest,
}
)
// New returns an instance of Registry
func New(endpoint string, client *http.Client) (*Registry, error) {
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
return &Registry{
Endpoint: u,
client: client,
ub: &uRLBuilder{
root: u,
},
}, nil
}
// Catalog ...
func (r *Registry) Catalog() ([]string, error) {
repos := []string{}
req, err := http.NewRequest("GET", r.ub.buildCatalogURL(), nil)
if err != nil {
return repos, err
}
resp, err := r.client.Do(req)
if err != nil {
return repos, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return repos, err
}
if resp.StatusCode == http.StatusOK {
catalogResp := struct {
Repositories []string `json:"repositories"`
}{}
if err := json.Unmarshal(b, &catalogResp); err != nil {
return repos, err
}
repos = catalogResp.Repositories
return repos, nil
}
return repos, errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
// ListTag ...
func (r *Registry) ListTag(name string) ([]string, error) {
tags := []string{}
req, err := http.NewRequest("GET", r.ub.buildTagListURL(name), nil)
if err != nil {
return tags, err
}
resp, err := r.client.Do(req)
if err != nil {
return tags, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return tags, err
}
if resp.StatusCode == http.StatusOK {
tagsResp := struct {
Tags []string `json:"tags"`
}{}
if err := json.Unmarshal(b, &tagsResp); err != nil {
return tags, err
}
tags = tagsResp.Tags
return tags, nil
}
return tags, errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
// ManifestExist ...
func (r *Registry) ManifestExist(name, reference string) (digest string, exist bool, err error) {
req, err := http.NewRequest("HEAD", r.ub.buildManifestURL(name, reference), nil)
if err != nil {
return
}
// request Schema 2 manifest, if the registry does not support it,
// Schema 1 manifest will be returned
req.Header.Set(http.CanonicalHeaderKey("Accept"), schema2.MediaTypeManifest)
resp, err := r.client.Do(req)
if err != nil {
return
}
if resp.StatusCode == http.StatusOK {
exist = true
digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
return
}
if resp.StatusCode == http.StatusNotFound {
return
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
err = errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
return
}
// PullManifest ...
func (r *Registry) PullManifest(name, reference string, version manifest.Versioned) (digest, mediaType string, payload []byte, err error) {
req, err := http.NewRequest("GET", r.ub.buildManifestURL(name, reference), nil)
if err != nil {
return
}
// if the registry does not support schema 2, schema 1 manifest will be returned
req.Header.Set(http.CanonicalHeaderKey("Accept"), version.MediaType)
resp, err := r.client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
if resp.StatusCode == http.StatusOK {
digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
mediaType = resp.Header.Get(http.CanonicalHeaderKey("Content-Type"))
payload = b
return
}
err = errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
return
}
// DeleteManifest ...
func (r *Registry) DeleteManifest(name, digest string) error {
req, err := http.NewRequest("DELETE", r.ub.buildManifestURL(name, digest), nil)
if err != nil {
return err
}
resp, err := r.client.Do(req)
if err != nil {
return err
}
if resp.StatusCode == http.StatusAccepted {
return nil
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
// DeleteTag ...
func (r *Registry) DeleteTag(name, tag string) error {
digest, exist, err := r.ManifestExist(name, tag)
if err != nil {
return err
}
if !exist {
return errors.Error{
StatusCode: http.StatusNotFound,
}
}
return r.DeleteManifest(name, digest)
}
// DeleteBlob ...
func (r *Registry) DeleteBlob(name, digest string) error {
req, err := http.NewRequest("DELETE", r.ub.buildBlobURL(name, digest), nil)
if err != nil {
return err
}
resp, err := r.client.Do(req)
if err != nil {
return err
}
if resp.StatusCode == http.StatusAccepted {
return nil
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return errors.Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
func (u *uRLBuilder) buildCatalogURL() string {
return fmt.Sprintf("%s/v2/_catalog", u.root.String())
}
func (u *uRLBuilder) buildTagListURL(name string) string {
return fmt.Sprintf("%s/v2/%s/tags/list", u.root.String(), name)
}
func (u *uRLBuilder) buildManifestURL(name, reference string) string {
return fmt.Sprintf("%s/v2/%s/manifests/%s", u.root.String(), name, reference)
}
func (u *uRLBuilder) buildBlobURL(name, reference string) string {
return fmt.Sprintf("%s/v2/%s/blobs/%s", u.root.String(), name, reference)
}