registry v2 API utility

This commit is contained in:
Wenkai Yin 2016-04-13 14:43:17 +08:00
parent 29cb4f28e5
commit dc293e2289
5 changed files with 479 additions and 0 deletions

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,131 @@
/*
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"
"net/http"
"net/url"
"strings"
"github.com/vmware/harbor/utils/log"
)
// Handler authorizes the request when encounters a 401 error
type Handler interface {
// Scheme : basic, bearer
Scheme() 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 struct {
// Username ...
Username string
// Password ...
Password string
//SecretKey ...
SecretKey string
}
type token struct {
Token string `json:"token"`
}
type tokenHandler struct {
client *http.Client
credential *Credential
}
// NewTokenHandler ...
// TODO deal with https
func NewTokenHandler(credential *Credential) Handler {
return &tokenHandler{
client: &http.Client{
Transport: http.DefaultTransport,
},
credential: credential,
}
}
// Scheme : see interface AuthHandler
func (t *tokenHandler) Scheme() string {
return "bearer"
}
// AuthorizeRequest : see interface AuthHandler
func (t *tokenHandler) 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
}
// TODO support secretKey
if len(t.credential.Username) != 0 {
r.SetBasicAuth(t.credential.Username, t.credential.Password)
}
resp, err := t.client.Do(r)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("error occured when get token from %s, status code: %d, status info: %s",
realm, resp.StatusCode, resp.Status)
}
defer resp.Body.Close()
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("request token successfully | %s %s", req.Method, req.URL)
return nil
}

View File

@ -0,0 +1,99 @@
/*
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"
)
// NewHTTPClientAuthHandlersEmbeded return a http.Client which will authorize the request
// and send it again when encounters a 401 error
func NewHTTPClientAuthHandlersEmbeded(credential *auth.Credential) *http.Client {
handlers := []auth.Handler{}
tokenHandler := auth.NewTokenHandler(credential)
handlers = append(handlers, tokenHandler)
transport := NewAuthTransport(http.DefaultTransport, handlers)
return &http.Client{
Transport: transport,
}
}
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.Scheme() {
log.Debugf("scheme not match: %s %s, skip", scheme, handler.Scheme())
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)
}

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

@ -0,0 +1,192 @@
/*
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 (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/docker/distribution/manifest/schema2"
)
// Registry holds information of a registry entiry
type Registry struct {
Endpoint *url.URL
client *http.Client
ub *uRLBuilder
}
type uRLBuilder struct {
root *url.URL
}
// 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
}
// 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
}
// Schema 2 manifest
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()
message, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
err = fmt.Errorf("%s %s : %s %s", req.Method, req.URL, resp.Status, string(message))
return
}
// PullManifest ...
func (r *Registry) PullManifest(name, reference string) (digest, mediaType string, payload []byte, err error) {
req, err := http.NewRequest("GET", 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
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
mediaType = resp.Header.Get(http.CanonicalHeaderKey("Content-Type"))
payload, err = ioutil.ReadAll(resp.Body)
return
}
message, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
err = fmt.Errorf("%s %s : %s %s", req.Method, req.URL, resp.Status, string(message))
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()
message, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("%s %s : %s %s", req.Method, req.URL, resp.Status, string(message))
}
// DeleteTag ...
func (r *Registry) DeleteTag(name, tag string) error {
digest, _, err := r.ManifestExist(name, tag)
if err != nil {
return err
}
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()
message, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("%s %s : %s %s", req.Method, req.URL, resp.Status, string(message))
}
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)
}