remotes: support cross-repo-push
With distribution source label in content store, select the longest common prefix components as condidate mount blob source and try to push with mount blob. Fix #2964 Signed-off-by: Wei Fu <fuweid89@gmail.com>
This commit is contained in:
parent
545e79ae11
commit
dd7c0aabcc
@ -44,7 +44,8 @@ type dockerAuthorizer struct {
|
|||||||
ua string
|
ua string
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
auth map[string]string
|
// indexed by host name
|
||||||
|
handlers map[string]*authHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthorizer creates a Docker authorizer using the provided function to
|
// NewAuthorizer creates a Docker authorizer using the provided function to
|
||||||
@ -53,116 +54,226 @@ func NewAuthorizer(client *http.Client, f func(string) (string, string, error))
|
|||||||
if client == nil {
|
if client == nil {
|
||||||
client = http.DefaultClient
|
client = http.DefaultClient
|
||||||
}
|
}
|
||||||
|
|
||||||
return &dockerAuthorizer{
|
return &dockerAuthorizer{
|
||||||
credentials: f,
|
credentials: f,
|
||||||
client: client,
|
client: client,
|
||||||
ua: "containerd/" + version.Version,
|
ua: "containerd/" + version.Version,
|
||||||
auth: map[string]string{},
|
handlers: make(map[string]*authHandler),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authorize handles auth request.
|
||||||
func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) error {
|
func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) error {
|
||||||
// TODO: Lookup matching challenge and scope rather than just host
|
// skip if there is no auth handler
|
||||||
if auth := a.getAuth(req.URL.Host); auth != "" {
|
ah := a.getAuthHandler(req.URL.Host)
|
||||||
req.Header.Set("Authorization", auth)
|
if ah == nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth, err := ah.authorize(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", auth)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *dockerAuthorizer) getAuthHandler(host string) *authHandler {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
return a.handlers[host]
|
||||||
|
}
|
||||||
|
|
||||||
func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error {
|
func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error {
|
||||||
last := responses[len(responses)-1]
|
last := responses[len(responses)-1]
|
||||||
host := last.Request.URL.Host
|
host := last.Request.URL.Host
|
||||||
|
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
for _, c := range parseAuthHeader(last.Header) {
|
for _, c := range parseAuthHeader(last.Header) {
|
||||||
if c.scheme == bearerAuth {
|
if c.scheme == bearerAuth {
|
||||||
if err := invalidAuthorization(c, responses); err != nil {
|
if err := invalidAuthorization(c, responses); err != nil {
|
||||||
// TODO: Clear token
|
delete(a.handlers, host)
|
||||||
a.setAuth(host, "")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(dmcg): Store challenge, not token
|
// reuse existing handler
|
||||||
// Move token fetching to authorize
|
//
|
||||||
return a.setTokenAuth(ctx, host, c.parameters)
|
// assume that one registry will return the common
|
||||||
|
// challenge information, including realm and service.
|
||||||
|
// and the resource scope is only different part
|
||||||
|
// which can be provided by each request.
|
||||||
|
if _, ok := a.handlers[host]; ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
common, err := a.generateTokenOptions(ctx, host, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.handlers[host] = newAuthHandler(a.client, a.ua, c.scheme, common)
|
||||||
|
return nil
|
||||||
} else if c.scheme == basicAuth && a.credentials != nil {
|
} else if c.scheme == basicAuth && a.credentials != nil {
|
||||||
// TODO: Resolve credentials on authorize
|
|
||||||
username, secret, err := a.credentials(host)
|
username, secret, err := a.credentials(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if username != "" && secret != "" {
|
if username != "" && secret != "" {
|
||||||
auth := username + ":" + secret
|
common := tokenOptions{
|
||||||
a.setAuth(host, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(auth))))
|
username: username,
|
||||||
|
secret: secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
a.handlers[host] = newAuthHandler(a.client, a.ua, c.scheme, common)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
|
return errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *dockerAuthorizer) getAuth(host string) string {
|
func (a *dockerAuthorizer) generateTokenOptions(ctx context.Context, host string, c challenge) (tokenOptions, error) {
|
||||||
a.mu.Lock()
|
realm, ok := c.parameters["realm"]
|
||||||
defer a.mu.Unlock()
|
|
||||||
|
|
||||||
return a.auth[host]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *dockerAuthorizer) setAuth(host string, auth string) bool {
|
|
||||||
a.mu.Lock()
|
|
||||||
defer a.mu.Unlock()
|
|
||||||
|
|
||||||
changed := a.auth[host] != auth
|
|
||||||
a.auth[host] = auth
|
|
||||||
|
|
||||||
return changed
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *dockerAuthorizer) setTokenAuth(ctx context.Context, host string, params map[string]string) error {
|
|
||||||
realm, ok := params["realm"]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("no realm specified for token auth challenge")
|
return tokenOptions{}, errors.New("no realm specified for token auth challenge")
|
||||||
}
|
}
|
||||||
|
|
||||||
realmURL, err := url.Parse(realm)
|
realmURL, err := url.Parse(realm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "invalid token auth challenge realm")
|
return tokenOptions{}, errors.Wrap(err, "invalid token auth challenge realm")
|
||||||
}
|
}
|
||||||
|
|
||||||
to := tokenOptions{
|
to := tokenOptions{
|
||||||
realm: realmURL.String(),
|
realm: realmURL.String(),
|
||||||
service: params["service"],
|
service: c.parameters["service"],
|
||||||
}
|
}
|
||||||
|
|
||||||
to.scopes = getTokenScopes(ctx, params)
|
scope, ok := c.parameters["scope"]
|
||||||
if len(to.scopes) == 0 {
|
if !ok {
|
||||||
return errors.Errorf("no scope specified for token auth challenge")
|
return tokenOptions{}, errors.Errorf("no scope specified for token auth challenge")
|
||||||
}
|
}
|
||||||
|
to.scopes = append(to.scopes, scope)
|
||||||
|
|
||||||
if a.credentials != nil {
|
if a.credentials != nil {
|
||||||
to.username, to.secret, err = a.credentials(host)
|
to.username, to.secret, err = a.credentials(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return tokenOptions{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return to, nil
|
||||||
|
}
|
||||||
|
|
||||||
var token string
|
// authResult is used to control limit rate.
|
||||||
|
type authResult struct {
|
||||||
|
sync.WaitGroup
|
||||||
|
token string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// authHandler is used to handle auth request per registry server.
|
||||||
|
type authHandler struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
ua string
|
||||||
|
|
||||||
|
client *http.Client
|
||||||
|
|
||||||
|
// only support basic and bearer schemes
|
||||||
|
scheme authenticationScheme
|
||||||
|
|
||||||
|
// common contains common challenge answer
|
||||||
|
common tokenOptions
|
||||||
|
|
||||||
|
// scopedTokens caches token indexed by scopes, which used in
|
||||||
|
// bearer auth case
|
||||||
|
scopedTokens map[string]*authResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthHandler(client *http.Client, ua string, scheme authenticationScheme, opts tokenOptions) *authHandler {
|
||||||
|
if client == nil {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return &authHandler{
|
||||||
|
ua: ua,
|
||||||
|
client: client,
|
||||||
|
scheme: scheme,
|
||||||
|
common: opts,
|
||||||
|
scopedTokens: map[string]*authResult{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *authHandler) authorize(ctx context.Context) (string, error) {
|
||||||
|
switch ah.scheme {
|
||||||
|
case basicAuth:
|
||||||
|
return ah.doBasicAuth(ctx)
|
||||||
|
case bearerAuth:
|
||||||
|
return ah.doBearerAuth(ctx)
|
||||||
|
default:
|
||||||
|
return "", errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *authHandler) doBasicAuth(ctx context.Context) (string, error) {
|
||||||
|
username, secret := ah.common.username, ah.common.secret
|
||||||
|
|
||||||
|
if username == "" || secret == "" {
|
||||||
|
return "", fmt.Errorf("failed to handle basic auth because missing username or secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + secret))
|
||||||
|
return fmt.Sprintf("%s %s", "Basic", auth), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *authHandler) doBearerAuth(ctx context.Context) (string, error) {
|
||||||
|
// copy common tokenOptions
|
||||||
|
to := ah.common
|
||||||
|
|
||||||
|
to.scopes = getTokenScopes(ctx, to.scopes)
|
||||||
|
if len(to.scopes) == 0 {
|
||||||
|
return "", errors.Errorf("no scope specified for token auth challenge")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docs: https://docs.docker.com/registry/spec/auth/scope
|
||||||
|
scoped := strings.Join(to.scopes, " ")
|
||||||
|
|
||||||
|
ah.Lock()
|
||||||
|
if r, exist := ah.scopedTokens[scoped]; exist {
|
||||||
|
ah.Unlock()
|
||||||
|
r.Wait()
|
||||||
|
return r.token, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// only one fetch token job
|
||||||
|
r := new(authResult)
|
||||||
|
r.Add(1)
|
||||||
|
ah.scopedTokens[scoped] = r
|
||||||
|
ah.Unlock()
|
||||||
|
|
||||||
|
// fetch token for the resource scope
|
||||||
|
var (
|
||||||
|
token string
|
||||||
|
err error
|
||||||
|
)
|
||||||
if to.secret != "" {
|
if to.secret != "" {
|
||||||
// Credential information is provided, use oauth POST endpoint
|
// credential information is provided, use oauth POST endpoint
|
||||||
token, err = a.fetchTokenWithOAuth(ctx, to)
|
token, err = ah.fetchTokenWithOAuth(ctx, to)
|
||||||
if err != nil {
|
err = errors.Wrap(err, "failed to fetch oauth token")
|
||||||
return errors.Wrap(err, "failed to fetch oauth token")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Do request anonymously
|
// do request anonymously
|
||||||
token, err = a.fetchToken(ctx, to)
|
token, err = ah.fetchToken(ctx, to)
|
||||||
if err != nil {
|
err = errors.Wrap(err, "failed to fetch anonymous token")
|
||||||
return errors.Wrap(err, "failed to fetch anonymous token")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
a.setAuth(host, fmt.Sprintf("Bearer %s", token))
|
token = fmt.Sprintf("%s %s", "Bearer", token)
|
||||||
|
|
||||||
return nil
|
r.token, r.err = token, err
|
||||||
|
r.Done()
|
||||||
|
return r.token, r.err
|
||||||
}
|
}
|
||||||
|
|
||||||
type tokenOptions struct {
|
type tokenOptions struct {
|
||||||
@ -181,7 +292,7 @@ type postTokenResponse struct {
|
|||||||
Scope string `json:"scope"`
|
Scope string `json:"scope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *dockerAuthorizer) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
|
func (ah *authHandler) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
|
||||||
form := url.Values{}
|
form := url.Values{}
|
||||||
form.Set("scope", strings.Join(to.scopes, " "))
|
form.Set("scope", strings.Join(to.scopes, " "))
|
||||||
form.Set("service", to.service)
|
form.Set("service", to.service)
|
||||||
@ -202,11 +313,11 @@ func (a *dockerAuthorizer) fetchTokenWithOAuth(ctx context.Context, to tokenOpti
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
|
||||||
if a.ua != "" {
|
if ah.ua != "" {
|
||||||
req.Header.Set("User-Agent", a.ua)
|
req.Header.Set("User-Agent", ah.ua)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := ctxhttp.Do(ctx, a.client, req)
|
resp, err := ctxhttp.Do(ctx, ah.client, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -216,7 +327,7 @@ func (a *dockerAuthorizer) fetchTokenWithOAuth(ctx context.Context, to tokenOpti
|
|||||||
// As of September 2017, GCR is known to return 404.
|
// As of September 2017, GCR is known to return 404.
|
||||||
// As of February 2018, JFrog Artifactory is known to return 401.
|
// As of February 2018, JFrog Artifactory is known to return 401.
|
||||||
if (resp.StatusCode == 405 && to.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 {
|
if (resp.StatusCode == 405 && to.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 {
|
||||||
return a.fetchToken(ctx, to)
|
return ah.fetchToken(ctx, to)
|
||||||
} else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
} else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||||
b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB
|
b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB
|
||||||
log.G(ctx).WithFields(logrus.Fields{
|
log.G(ctx).WithFields(logrus.Fields{
|
||||||
@ -245,15 +356,15 @@ type getTokenResponse struct {
|
|||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// getToken fetches a token using a GET request
|
// fetchToken fetches a token using a GET request
|
||||||
func (a *dockerAuthorizer) fetchToken(ctx context.Context, to tokenOptions) (string, error) {
|
func (ah *authHandler) fetchToken(ctx context.Context, to tokenOptions) (string, error) {
|
||||||
req, err := http.NewRequest("GET", to.realm, nil)
|
req, err := http.NewRequest("GET", to.realm, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.ua != "" {
|
if ah.ua != "" {
|
||||||
req.Header.Set("User-Agent", a.ua)
|
req.Header.Set("User-Agent", ah.ua)
|
||||||
}
|
}
|
||||||
|
|
||||||
reqParams := req.URL.Query()
|
reqParams := req.URL.Query()
|
||||||
@ -272,7 +383,7 @@ func (a *dockerAuthorizer) fetchToken(ctx context.Context, to tokenOptions) (str
|
|||||||
|
|
||||||
req.URL.RawQuery = reqParams.Encode()
|
req.URL.RawQuery = reqParams.Encode()
|
||||||
|
|
||||||
resp, err := ctxhttp.Do(ctx, a.client, req)
|
resp, err := ctxhttp.Do(ctx, ah.client, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -110,3 +110,45 @@ func appendDistributionSourceLabel(originLabel, repo string) string {
|
|||||||
func distributionSourceLabelKey(source string) string {
|
func distributionSourceLabelKey(source string) string {
|
||||||
return fmt.Sprintf("%s.%s", labelDistributionSource, source)
|
return fmt.Sprintf("%s.%s", labelDistributionSource, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// selectRepositoryMountCandidate will select the repo which has longest
|
||||||
|
// common prefix components as the candidate.
|
||||||
|
func selectRepositoryMountCandidate(refspec reference.Spec, sources map[string]string) string {
|
||||||
|
u, err := url.Parse("dummy://" + refspec.Locator)
|
||||||
|
if err != nil {
|
||||||
|
// NOTE: basically, it won't be error here
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
source, target := u.Hostname(), strings.TrimPrefix(u.Path, "/")
|
||||||
|
repoLabel, ok := sources[distributionSourceLabelKey(source)]
|
||||||
|
if !ok || repoLabel == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
n, match := 0, ""
|
||||||
|
components := strings.Split(target, "/")
|
||||||
|
for _, repo := range strings.Split(repoLabel, ",") {
|
||||||
|
// the target repo is not a candidate
|
||||||
|
if repo == target {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := commonPrefixComponents(components, repo); l >= n {
|
||||||
|
n, match = l, repo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
func commonPrefixComponents(components []string, target string) int {
|
||||||
|
targetComponents := strings.Split(target, "/")
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for ; i < len(components) && i < len(targetComponents); i++ {
|
||||||
|
if components[i] != targetComponents[i] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
@ -116,8 +116,6 @@ func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (conten
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Lookup related objects for cross repository push
|
|
||||||
|
|
||||||
if isManifest {
|
if isManifest {
|
||||||
var putPath string
|
var putPath string
|
||||||
if p.tag != "" {
|
if p.tag != "" {
|
||||||
@ -132,21 +130,57 @@ func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor) (conten
|
|||||||
}
|
}
|
||||||
req.Header.Add("Content-Type", desc.MediaType)
|
req.Header.Add("Content-Type", desc.MediaType)
|
||||||
} else {
|
} else {
|
||||||
// TODO: Do monolithic upload if size is small
|
|
||||||
|
|
||||||
// Start upload request
|
// Start upload request
|
||||||
req, err = http.NewRequest(http.MethodPost, p.url("blobs", "uploads")+"/", nil)
|
req, err = http.NewRequest(http.MethodPost, p.url("blobs", "uploads")+"/", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := p.doRequestWithRetries(ctx, req, nil)
|
var resp *http.Response
|
||||||
if err != nil {
|
if fromRepo := selectRepositoryMountCandidate(p.refspec, desc.Annotations); fromRepo != "" {
|
||||||
return nil, err
|
req = requestWithMountFrom(req, desc.Digest.String(), fromRepo)
|
||||||
|
pctx := contextWithAppendPullRepositoryScope(ctx, fromRepo)
|
||||||
|
|
||||||
|
// NOTE: the fromRepo might be private repo and
|
||||||
|
// auth service still can grant token without error.
|
||||||
|
// but the post request will fail because of 401.
|
||||||
|
//
|
||||||
|
// for the private repo, we should remove mount-from
|
||||||
|
// query and send the request again.
|
||||||
|
resp, err = p.doRequest(pctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized {
|
||||||
|
log.G(ctx).Debugf("failed to mount from repository %s", fromRepo)
|
||||||
|
|
||||||
|
resp.Body.Close()
|
||||||
|
resp = nil
|
||||||
|
|
||||||
|
req, err = removeMountFromQuery(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp == nil {
|
||||||
|
resp, err = p.doRequestWithRetries(ctx, req, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
case http.StatusOK, http.StatusAccepted, http.StatusNoContent:
|
case http.StatusOK, http.StatusAccepted, http.StatusNoContent:
|
||||||
|
case http.StatusCreated:
|
||||||
|
p.tracker.SetStatus(ref, Status{
|
||||||
|
Status: content.Status{
|
||||||
|
Ref: ref,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v on remote", desc.Digest)
|
||||||
default:
|
default:
|
||||||
// TODO: log error
|
// TODO: log error
|
||||||
return nil, errors.Errorf("unexpected response: %s", resp.Status)
|
return nil, errors.Errorf("unexpected response: %s", resp.Status)
|
||||||
@ -320,3 +354,25 @@ func (pw *pushWriter) Truncate(size int64) error {
|
|||||||
// TODO: always error on manifest
|
// TODO: always error on manifest
|
||||||
return errors.New("cannot truncate remote upload")
|
return errors.New("cannot truncate remote upload")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requestWithMountFrom(req *http.Request, mount, from string) *http.Request {
|
||||||
|
q := req.URL.Query()
|
||||||
|
|
||||||
|
q.Set("mount", mount)
|
||||||
|
q.Set("from", from)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeMountFromQuery(req *http.Request) (*http.Request, error) {
|
||||||
|
req, err := copyRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Del("mount")
|
||||||
|
q.Del("from")
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@ package docker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@ -53,24 +54,38 @@ func contextWithRepositoryScope(ctx context.Context, refspec reference.Spec, pus
|
|||||||
return context.WithValue(ctx, tokenScopesKey{}, []string{s}), nil
|
return context.WithValue(ctx, tokenScopesKey{}, []string{s}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTokenScopes returns deduplicated and sorted scopes from ctx.Value(tokenScopesKey{}) and params["scope"].
|
// contextWithAppendPullRepositoryScope is used to append repository pull
|
||||||
func getTokenScopes(ctx context.Context, params map[string]string) []string {
|
// scope into existing scopes indexed by the tokenScopesKey{}.
|
||||||
|
func contextWithAppendPullRepositoryScope(ctx context.Context, repo string) context.Context {
|
||||||
|
var scopes []string
|
||||||
|
|
||||||
|
if v := ctx.Value(tokenScopesKey{}); v != nil {
|
||||||
|
scopes = append(scopes, v.([]string)...)
|
||||||
|
}
|
||||||
|
scopes = append(scopes, fmt.Sprintf("repository:%s:pull", repo))
|
||||||
|
return context.WithValue(ctx, tokenScopesKey{}, scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTokenScopes returns deduplicated and sorted scopes from ctx.Value(tokenScopesKey{}) and common scopes.
|
||||||
|
func getTokenScopes(ctx context.Context, common []string) []string {
|
||||||
var scopes []string
|
var scopes []string
|
||||||
if x := ctx.Value(tokenScopesKey{}); x != nil {
|
if x := ctx.Value(tokenScopesKey{}); x != nil {
|
||||||
scopes = append(scopes, x.([]string)...)
|
scopes = append(scopes, x.([]string)...)
|
||||||
}
|
}
|
||||||
if scope, ok := params["scope"]; ok {
|
|
||||||
for _, s := range scopes {
|
scopes = append(scopes, common...)
|
||||||
// Note: this comparison is unaware of the scope grammar (https://docs.docker.com/registry/spec/auth/scope/)
|
|
||||||
// So, "repository:foo/bar:pull,push" != "repository:foo/bar:push,pull", although semantically they are equal.
|
|
||||||
if s == scope {
|
|
||||||
// already appended
|
|
||||||
goto Sort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scopes = append(scopes, scope)
|
|
||||||
}
|
|
||||||
Sort:
|
|
||||||
sort.Strings(scopes)
|
sort.Strings(scopes)
|
||||||
return scopes
|
|
||||||
|
l := 0
|
||||||
|
for idx := 1; idx < len(scopes); idx++ {
|
||||||
|
// Note: this comparison is unaware of the scope grammar (https://docs.docker.com/registry/spec/auth/scope/)
|
||||||
|
// So, "repository:foo/bar:pull,push" != "repository:foo/bar:push,pull", although semantically they are equal.
|
||||||
|
if scopes[l] == scopes[idx] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
l++
|
||||||
|
scopes[l] = scopes[idx]
|
||||||
|
}
|
||||||
|
return scopes[:l+1]
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/containerd/containerd/reference"
|
"github.com/containerd/containerd/reference"
|
||||||
@ -54,3 +55,42 @@ func TestRepositoryScope(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetTokenScopes(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
scopesInCtx []string
|
||||||
|
commonScopes []string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scopesInCtx: []string{},
|
||||||
|
commonScopes: []string{"repository:foo/bar:pull"},
|
||||||
|
expected: []string{"repository:foo/bar:pull"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scopesInCtx: []string{"repository:foo/bar:pull,push"},
|
||||||
|
commonScopes: []string{},
|
||||||
|
expected: []string{"repository:foo/bar:pull,push"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scopesInCtx: []string{"repository:foo/bar:pull"},
|
||||||
|
commonScopes: []string{"repository:foo/bar:pull"},
|
||||||
|
expected: []string{"repository:foo/bar:pull"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scopesInCtx: []string{"repository:foo/bar:pull"},
|
||||||
|
commonScopes: []string{"repository:foo/bar:pull,push"},
|
||||||
|
expected: []string{"repository:foo/bar:pull", "repository:foo/bar:pull,push"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scopesInCtx: []string{"repository:foo/bar:pull"},
|
||||||
|
commonScopes: []string{"repository:foo/bar:pull,push", "repository:foo/bar:pull"},
|
||||||
|
expected: []string{"repository:foo/bar:pull", "repository:foo/bar:pull,push"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
ctx := context.WithValue(context.TODO(), tokenScopesKey{}, tc.scopesInCtx)
|
||||||
|
actual := getTokenScopes(ctx, tc.commonScopes)
|
||||||
|
assert.DeepEqual(t, tc.expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -156,7 +156,7 @@ func push(ctx context.Context, provider content.Provider, pusher Pusher, desc oc
|
|||||||
//
|
//
|
||||||
// Base handlers can be provided which will be called before any push specific
|
// Base handlers can be provided which will be called before any push specific
|
||||||
// handlers.
|
// handlers.
|
||||||
func PushContent(ctx context.Context, pusher Pusher, desc ocispec.Descriptor, provider content.Provider, platform platforms.MatchComparer, wrapper func(h images.Handler) images.Handler) error {
|
func PushContent(ctx context.Context, pusher Pusher, desc ocispec.Descriptor, store content.Store, platform platforms.MatchComparer, wrapper func(h images.Handler) images.Handler) error {
|
||||||
var m sync.Mutex
|
var m sync.Mutex
|
||||||
manifestStack := []ocispec.Descriptor{}
|
manifestStack := []ocispec.Descriptor{}
|
||||||
|
|
||||||
@ -173,10 +173,14 @@ func PushContent(ctx context.Context, pusher Pusher, desc ocispec.Descriptor, pr
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
pushHandler := PushHandler(pusher, provider)
|
pushHandler := PushHandler(pusher, store)
|
||||||
|
|
||||||
|
platformFilterhandler := images.FilterPlatforms(images.ChildrenHandler(store), platform)
|
||||||
|
|
||||||
|
annotateHandler := annotateDistributionSourceHandler(platformFilterhandler, store)
|
||||||
|
|
||||||
var handler images.Handler = images.Handlers(
|
var handler images.Handler = images.Handlers(
|
||||||
images.FilterPlatforms(images.ChildrenHandler(provider), platform),
|
annotateHandler,
|
||||||
filterHandler,
|
filterHandler,
|
||||||
pushHandler,
|
pushHandler,
|
||||||
)
|
)
|
||||||
@ -241,3 +245,45 @@ func FilterManifestByPlatformHandler(f images.HandlerFunc, m platforms.Matcher)
|
|||||||
return descs, nil
|
return descs, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// annotateDistributionSourceHandler add distribution source label into
|
||||||
|
// annotation of config or blob descriptor.
|
||||||
|
func annotateDistributionSourceHandler(f images.HandlerFunc, manager content.Manager) images.HandlerFunc {
|
||||||
|
return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||||
|
children, err := f(ctx, desc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// only add distribution source for the config or blob data descriptor
|
||||||
|
switch desc.MediaType {
|
||||||
|
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest,
|
||||||
|
images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
|
||||||
|
default:
|
||||||
|
return children, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range children {
|
||||||
|
child := children[i]
|
||||||
|
|
||||||
|
info, err := manager.Info(ctx, child.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range info.Labels {
|
||||||
|
if !strings.HasPrefix(k, "containerd.io/distribution.source.") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if child.Annotations == nil {
|
||||||
|
child.Annotations = map[string]string{}
|
||||||
|
}
|
||||||
|
child.Annotations[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
children[i] = child
|
||||||
|
}
|
||||||
|
return children, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user