mirror of
https://github.com/goharbor/harbor
synced 2025-05-21 14:13:40 +00:00
registry v2 API utility
This commit is contained in:
parent
29cb4f28e5
commit
dc293e2289
32
utils/registry/auth/challenge.go
Normal file
32
utils/registry/auth/challenge.go
Normal 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
|
||||
}
|
131
utils/registry/auth/handler.go
Normal file
131
utils/registry/auth/handler.go
Normal 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
|
||||
}
|
99
utils/registry/httpclient.go
Normal file
99
utils/registry/httpclient.go
Normal 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
|
||||
}
|
25
utils/registry/manifest.go
Normal file
25
utils/registry/manifest.go
Normal 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
192
utils/registry/registry.go
Normal 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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user