Merge pull request #783 from dmcgowan/resolver-authentication
Pull from all registries
This commit is contained in:
commit
0fe0d8feeb
95
cmd/dist/common.go
vendored
95
cmd/dist/common.go
vendored
@ -1,9 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
imagesapi "github.com/containerd/containerd/api/services/images"
|
||||
@ -12,10 +17,31 @@ import (
|
||||
"github.com/containerd/containerd/remotes"
|
||||
"github.com/containerd/containerd/remotes/docker"
|
||||
imagesservice "github.com/containerd/containerd/services/images"
|
||||
"github.com/crosbymichael/console"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var registryFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "skip-verify,k",
|
||||
Usage: "Skip SSL certificate validation",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "plain-http",
|
||||
Usage: "Allow connections using plain HTTP",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "user,u",
|
||||
Usage: "user[:password] Registry user and password",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "refresh",
|
||||
Usage: "Refresh token for authorization server",
|
||||
},
|
||||
}
|
||||
|
||||
func resolveContentStore(context *cli.Context) (*content.Store, error) {
|
||||
root := filepath.Join(context.GlobalString("root"), "content")
|
||||
if !filepath.IsAbs(root) {
|
||||
@ -50,6 +76,71 @@ func connectGRPC(context *cli.Context) (*grpc.ClientConn, error) {
|
||||
}
|
||||
|
||||
// getResolver prepares the resolver from the environment and options.
|
||||
func getResolver(ctx context.Context) (remotes.Resolver, error) {
|
||||
return docker.NewResolver(), nil
|
||||
func getResolver(ctx context.Context, clicontext *cli.Context) (remotes.Resolver, error) {
|
||||
username := clicontext.String("user")
|
||||
var secret string
|
||||
if i := strings.IndexByte(username, ':'); i > 0 {
|
||||
secret = username[i+1:]
|
||||
username = username[0:i]
|
||||
}
|
||||
options := docker.ResolverOptions{
|
||||
PlainHTTP: clicontext.Bool("plain-http"),
|
||||
}
|
||||
if username != "" {
|
||||
if secret == "" {
|
||||
fmt.Printf("Password: ")
|
||||
|
||||
var err error
|
||||
secret, err = passwordPrompt()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Print("\n")
|
||||
}
|
||||
} else if rt := clicontext.String("refresh"); rt != "" {
|
||||
secret = rt
|
||||
}
|
||||
|
||||
options.Credentials = func(host string) (string, string, error) {
|
||||
// Only one host
|
||||
return username, secret, nil
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: clicontext.Bool("insecure"),
|
||||
},
|
||||
ExpectContinueTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
options.Client = &http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
|
||||
return docker.NewResolver(options), nil
|
||||
}
|
||||
|
||||
func passwordPrompt() (string, error) {
|
||||
c := console.Current()
|
||||
defer c.Reset()
|
||||
|
||||
if err := c.DisableEcho(); err != nil {
|
||||
return "", errors.Wrap(err, "failed to disable echo")
|
||||
}
|
||||
|
||||
line, _, err := bufio.NewReader(c).ReadLine()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to read line")
|
||||
}
|
||||
return string(line), nil
|
||||
}
|
||||
|
4
cmd/dist/fetch.go
vendored
4
cmd/dist/fetch.go
vendored
@ -39,7 +39,7 @@ not use this implementation as a guide. The end goal should be having metadata,
|
||||
content and snapshots ready for a direct use via the 'ctr run'.
|
||||
|
||||
Most of this is experimental and there are few leaps to make this work.`,
|
||||
Flags: []cli.Flag{},
|
||||
Flags: registryFlags,
|
||||
Action: func(clicontext *cli.Context) error {
|
||||
var (
|
||||
ctx = background
|
||||
@ -51,7 +51,7 @@ Most of this is experimental and there are few leaps to make this work.`,
|
||||
return err
|
||||
}
|
||||
|
||||
resolver, err := getResolver(ctx)
|
||||
resolver, err := getResolver(ctx, clicontext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
6
cmd/dist/fetchobject.go
vendored
6
cmd/dist/fetchobject.go
vendored
@ -18,13 +18,13 @@ var fetchObjectCommand = cli.Command{
|
||||
Usage: "retrieve objects from a remote",
|
||||
ArgsUsage: "[flags] <remote> <object> [<hint>, ...]",
|
||||
Description: `Fetch objects by identifier from a remote.`,
|
||||
Flags: []cli.Flag{
|
||||
Flags: append([]cli.Flag{
|
||||
cli.DurationFlag{
|
||||
Name: "timeout",
|
||||
Usage: "total timeout for fetch",
|
||||
EnvVar: "CONTAINERD_FETCH_TIMEOUT",
|
||||
},
|
||||
},
|
||||
}, registryFlags...),
|
||||
Action: func(context *cli.Context) error {
|
||||
var (
|
||||
ctx = background
|
||||
@ -38,7 +38,7 @@ var fetchObjectCommand = cli.Command{
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
resolver, err := getResolver(ctx)
|
||||
resolver, err := getResolver(ctx, context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
5
cmd/dist/pull.go
vendored
5
cmd/dist/pull.go
vendored
@ -35,7 +35,7 @@ command. As part of this process, we do the following:
|
||||
2. Prepare the snapshot filesystem with the pulled resources.
|
||||
3. Register metadata for the image.
|
||||
`,
|
||||
Flags: []cli.Flag{},
|
||||
Flags: registryFlags,
|
||||
Action: func(clicontext *cli.Context) error {
|
||||
var (
|
||||
ctx = background
|
||||
@ -52,7 +52,7 @@ command. As part of this process, we do the following:
|
||||
return err
|
||||
}
|
||||
|
||||
resolver, err := getResolver(ctx)
|
||||
resolver, err := getResolver(ctx, clicontext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -75,6 +75,7 @@ command. As part of this process, we do the following:
|
||||
ongoing.add(ref)
|
||||
name, desc, fetcher, err := resolver.Resolve(ctx, ref)
|
||||
if err != nil {
|
||||
log.G(ctx).WithError(err).Error("failed to resolve")
|
||||
return err
|
||||
}
|
||||
log.G(ctx).WithField("image", name).Debug("fetching")
|
||||
|
182
remotes/docker/auth.go
Normal file
182
remotes/docker/auth.go
Normal file
@ -0,0 +1,182 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type authenticationScheme byte
|
||||
|
||||
const (
|
||||
basicAuth authenticationScheme = 1 << iota // Defined in RFC 7617
|
||||
digestAuth // Defined in RFC 7616
|
||||
bearerAuth // Defined in RFC 6750
|
||||
)
|
||||
|
||||
// challenge carries information from a WWW-Authenticate response header.
|
||||
// See RFC 2617.
|
||||
type challenge struct {
|
||||
// scheme is the auth-scheme according to RFC 2617
|
||||
scheme authenticationScheme
|
||||
|
||||
// parameters are the auth-params according to RFC 2617
|
||||
parameters map[string]string
|
||||
}
|
||||
|
||||
type byScheme []challenge
|
||||
|
||||
func (bs byScheme) Len() int { return len(bs) }
|
||||
func (bs byScheme) Swap(i, j int) { bs[i], bs[j] = bs[j], bs[i] }
|
||||
|
||||
// Sort in priority order: token > digest > basic
|
||||
func (bs byScheme) Less(i, j int) bool { return bs[i].scheme > bs[j].scheme }
|
||||
|
||||
// Octet types from RFC 2616.
|
||||
type octetType byte
|
||||
|
||||
var octetTypes [256]octetType
|
||||
|
||||
const (
|
||||
isToken octetType = 1 << iota
|
||||
isSpace
|
||||
)
|
||||
|
||||
func init() {
|
||||
// OCTET = <any 8-bit sequence of data>
|
||||
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||
// CR = <US-ASCII CR, carriage return (13)>
|
||||
// LF = <US-ASCII LF, linefeed (10)>
|
||||
// SP = <US-ASCII SP, space (32)>
|
||||
// HT = <US-ASCII HT, horizontal-tab (9)>
|
||||
// <"> = <US-ASCII double-quote mark (34)>
|
||||
// CRLF = CR LF
|
||||
// LWS = [CRLF] 1*( SP | HT )
|
||||
// TEXT = <any OCTET except CTLs, but including LWS>
|
||||
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
||||
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
||||
// token = 1*<any CHAR except CTLs or separators>
|
||||
// qdtext = <any TEXT except <">>
|
||||
|
||||
for c := 0; c < 256; c++ {
|
||||
var t octetType
|
||||
isCtl := c <= 31 || c == 127
|
||||
isChar := 0 <= c && c <= 127
|
||||
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
|
||||
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
|
||||
t |= isSpace
|
||||
}
|
||||
if isChar && !isCtl && !isSeparator {
|
||||
t |= isToken
|
||||
}
|
||||
octetTypes[c] = t
|
||||
}
|
||||
}
|
||||
|
||||
func parseAuthHeader(header http.Header) []challenge {
|
||||
challenges := []challenge{}
|
||||
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
|
||||
v, p := parseValueAndParams(h)
|
||||
var s authenticationScheme
|
||||
switch v {
|
||||
case "basic":
|
||||
s = basicAuth
|
||||
case "digest":
|
||||
s = digestAuth
|
||||
case "bearer":
|
||||
s = bearerAuth
|
||||
default:
|
||||
continue
|
||||
}
|
||||
challenges = append(challenges, challenge{scheme: s, parameters: p})
|
||||
}
|
||||
sort.Stable(byScheme(challenges))
|
||||
return challenges
|
||||
}
|
||||
|
||||
func parseValueAndParams(header string) (value string, params map[string]string) {
|
||||
params = make(map[string]string)
|
||||
value, s := expectToken(header)
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
value = strings.ToLower(value)
|
||||
for {
|
||||
var pkey string
|
||||
pkey, s = expectToken(skipSpace(s))
|
||||
if pkey == "" {
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(s, "=") {
|
||||
return
|
||||
}
|
||||
var pvalue string
|
||||
pvalue, s = expectTokenOrQuoted(s[1:])
|
||||
if pvalue == "" {
|
||||
return
|
||||
}
|
||||
pkey = strings.ToLower(pkey)
|
||||
params[pkey] = pvalue
|
||||
s = skipSpace(s)
|
||||
if !strings.HasPrefix(s, ",") {
|
||||
return
|
||||
}
|
||||
s = s[1:]
|
||||
}
|
||||
}
|
||||
|
||||
func skipSpace(s string) (rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isSpace == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[i:]
|
||||
}
|
||||
|
||||
func expectToken(s string) (token, rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isToken == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[:i], s[i:]
|
||||
}
|
||||
|
||||
func expectTokenOrQuoted(s string) (value string, rest string) {
|
||||
if !strings.HasPrefix(s, "\"") {
|
||||
return expectToken(s)
|
||||
}
|
||||
s = s[1:]
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '"':
|
||||
return s[:i], s[i+1:]
|
||||
case '\\':
|
||||
p := make([]byte, len(s)-1)
|
||||
j := copy(p, s[:i])
|
||||
escape := true
|
||||
for i = i + 1; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch {
|
||||
case escape:
|
||||
escape = false
|
||||
p[j] = b
|
||||
j++
|
||||
case b == '\\':
|
||||
escape = true
|
||||
case b == '"':
|
||||
return string(p[:j]), s[i+1:]
|
||||
default:
|
||||
p[j] = b
|
||||
j++
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
@ -7,10 +7,12 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/containerd/containerd/images"
|
||||
@ -23,15 +25,43 @@ import (
|
||||
"golang.org/x/net/context/ctxhttp"
|
||||
)
|
||||
|
||||
// NOTE(stevvooe): Most of the code below this point is prototype code to
|
||||
// demonstrate a very simplified docker.io fetcher. We have a lot of hard coded
|
||||
// values but we leave many of the details down to the fetcher, creating a lot
|
||||
// of room for ways to fetch content.
|
||||
var (
|
||||
// ErrNoToken is returned if a request is successful but the body does not
|
||||
// contain an authorization token.
|
||||
ErrNoToken = errors.New("authorization server did not include a token in the response")
|
||||
|
||||
type dockerResolver struct{}
|
||||
// ErrInvalidAuthorization is used when credentials are passed to a server but
|
||||
// those credentials are rejected.
|
||||
ErrInvalidAuthorization = errors.New("authorization failed")
|
||||
)
|
||||
|
||||
func NewResolver() remotes.Resolver {
|
||||
return &dockerResolver{}
|
||||
type dockerResolver struct {
|
||||
credentials func(string) (string, string, error)
|
||||
plainHTTP bool
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// ResolverOptions are used to configured a new Docker register resolver
|
||||
type ResolverOptions struct {
|
||||
// Credentials provides username and secret given a host.
|
||||
// If username is empty but a secret is given, that secret
|
||||
// is interpretted as a long lived token.
|
||||
Credentials func(string) (string, string, error)
|
||||
|
||||
// PlainHTTP specifies to use plain http and not https
|
||||
PlainHTTP bool
|
||||
|
||||
// Client is the http client to used when making registry requests
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// NewResolver returns a new resolver to a Docker registry
|
||||
func NewResolver(options ResolverOptions) remotes.Resolver {
|
||||
return &dockerResolver{
|
||||
credentials: options.Credentials,
|
||||
plainHTTP: options.PlainHTTP,
|
||||
client: options.Client,
|
||||
}
|
||||
}
|
||||
|
||||
var _ remotes.Resolver = &dockerResolver{}
|
||||
@ -43,31 +73,38 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
||||
}
|
||||
|
||||
var (
|
||||
base url.URL
|
||||
token string
|
||||
base url.URL
|
||||
username, secret string
|
||||
)
|
||||
|
||||
switch refspec.Hostname() {
|
||||
case "docker.io":
|
||||
base.Scheme = "https"
|
||||
host := refspec.Hostname()
|
||||
base.Scheme = "https"
|
||||
|
||||
if host == "docker.io" {
|
||||
base.Host = "registry-1.docker.io"
|
||||
prefix := strings.TrimPrefix(refspec.Locator, "docker.io/")
|
||||
base.Path = path.Join("/v2", prefix)
|
||||
token, err = getToken(ctx, "repository:"+prefix+":pull")
|
||||
} else {
|
||||
base.Host = host
|
||||
|
||||
if r.plainHTTP || strings.HasPrefix(host, "localhost:") {
|
||||
base.Scheme = "http"
|
||||
}
|
||||
}
|
||||
|
||||
if r.credentials != nil {
|
||||
username, secret, err = r.credentials(base.Host)
|
||||
if err != nil {
|
||||
return "", ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
case "localhost:5000":
|
||||
base.Scheme = "http"
|
||||
base.Host = "localhost:5000"
|
||||
base.Path = path.Join("/v2", strings.TrimPrefix(refspec.Locator, "localhost:5000/"))
|
||||
default:
|
||||
return "", ocispec.Descriptor{}, nil, errors.Errorf("unsupported locator: %q", refspec.Locator)
|
||||
}
|
||||
|
||||
prefix := strings.TrimPrefix(refspec.Locator, host+"/")
|
||||
base.Path = path.Join("/v2", prefix)
|
||||
|
||||
fetcher := &dockerFetcher{
|
||||
base: base,
|
||||
token: token,
|
||||
base: base,
|
||||
client: r.client,
|
||||
username: username,
|
||||
secret: secret,
|
||||
}
|
||||
|
||||
var (
|
||||
@ -125,16 +162,13 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
||||
|
||||
if dgstHeader != "" {
|
||||
if err := dgstHeader.Validate(); err != nil {
|
||||
if err == nil {
|
||||
return "", ocispec.Descriptor{}, nil, errors.Errorf("%q in header not a valid digest", dgstHeader)
|
||||
}
|
||||
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader)
|
||||
}
|
||||
dgst = dgstHeader
|
||||
}
|
||||
|
||||
if dgst == "" {
|
||||
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "could not resolve digest for %v", ref)
|
||||
return "", ocispec.Descriptor{}, nil, errors.Errorf("could not resolve digest for %v", ref)
|
||||
}
|
||||
|
||||
var (
|
||||
@ -143,8 +177,12 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
||||
)
|
||||
|
||||
size, err = strconv.ParseInt(sizeHeader, 10, 64)
|
||||
if err != nil || size < 0 {
|
||||
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "%q in header not a valid size", sizeHeader)
|
||||
if err != nil {
|
||||
|
||||
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "invalid size header: %q", sizeHeader)
|
||||
}
|
||||
if size < 0 {
|
||||
return "", ocispec.Descriptor{}, nil, errors.Errorf("%q in header not a valid size", sizeHeader)
|
||||
}
|
||||
|
||||
desc := ocispec.Descriptor{
|
||||
@ -163,6 +201,11 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
|
||||
type dockerFetcher struct {
|
||||
base url.URL
|
||||
token string
|
||||
|
||||
client *http.Client
|
||||
useBasic bool
|
||||
username string
|
||||
secret string
|
||||
}
|
||||
|
||||
func (r *dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
|
||||
@ -213,13 +256,14 @@ func (r *dockerFetcher) url(ps ...string) string {
|
||||
}
|
||||
|
||||
func (r *dockerFetcher) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||
return r.doRequestWithRetries(ctx, req, nil)
|
||||
}
|
||||
|
||||
func (r *dockerFetcher) doRequestWithRetries(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Response, error) {
|
||||
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String()))
|
||||
log.G(ctx).WithField("request.headers", req.Header).Debug("fetch content")
|
||||
if r.token != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.token))
|
||||
}
|
||||
|
||||
resp, err := ctxhttp.Do(ctx, http.DefaultClient, req)
|
||||
r.authorize(req)
|
||||
resp, err := ctxhttp.Do(ctx, r.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -228,50 +272,119 @@ func (r *dockerFetcher) doRequest(ctx context.Context, req *http.Request) (*http
|
||||
"response.headers": resp.Header,
|
||||
}).Debug("fetch response received")
|
||||
|
||||
responses = append(responses, resp)
|
||||
req, err = r.retryRequest(ctx, req, responses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req != nil {
|
||||
return r.doRequestWithRetries(ctx, req, responses)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func getToken(ctx context.Context, scopes ...string) (string, error) {
|
||||
var (
|
||||
u = url.URL{
|
||||
Scheme: "https",
|
||||
Host: "auth.docker.io",
|
||||
Path: "/token",
|
||||
func (r *dockerFetcher) authorize(req *http.Request) {
|
||||
if r.useBasic {
|
||||
req.SetBasicAuth(r.username, r.secret)
|
||||
} else if r.token != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.token))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *dockerFetcher) retryRequest(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Request, error) {
|
||||
if len(responses) > 5 {
|
||||
return nil, nil
|
||||
}
|
||||
last := responses[len(responses)-1]
|
||||
if last.StatusCode == http.StatusUnauthorized {
|
||||
log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
|
||||
for _, c := range parseAuthHeader(last.Header) {
|
||||
if c.scheme == bearerAuth {
|
||||
if errStr := c.parameters["error"]; errStr != "" {
|
||||
// TODO: handle expired case
|
||||
return nil, errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
|
||||
}
|
||||
if err := r.setTokenAuth(ctx, c.parameters); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return req, nil
|
||||
} else if c.scheme == basicAuth {
|
||||
if r.username != "" && r.secret != "" {
|
||||
r.useBasic = true
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
}
|
||||
|
||||
q = url.Values{
|
||||
"scope": scopes,
|
||||
"service": []string{"registry.docker.io"}, // usually comes from auth challenge
|
||||
return nil, nil
|
||||
} else if last.StatusCode == http.StatusMethodNotAllowed && req.Method == http.MethodHead {
|
||||
// Support registries which have not properly implemented the HEAD method for
|
||||
// manifests endpoint
|
||||
if strings.Contains(req.URL.Path, "/manifests/") {
|
||||
// TODO: copy request?
|
||||
req.Method = http.MethodGet
|
||||
return req, nil
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
// TODO: Handle 50x errors accounting for attempt history
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
log.G(ctx).WithField("token.url", u.String()).Debug("requesting token")
|
||||
resp, err := ctxhttp.Get(ctx, http.DefaultClient, u.String())
|
||||
func isManifestAccept(h http.Header) bool {
|
||||
for _, ah := range h[textproto.CanonicalMIMEHeaderKey("Accept")] {
|
||||
switch ah {
|
||||
case images.MediaTypeDockerSchema2Manifest:
|
||||
fallthrough
|
||||
case images.MediaTypeDockerSchema2ManifestList:
|
||||
fallthrough
|
||||
case ocispec.MediaTypeImageManifest:
|
||||
fallthrough
|
||||
case ocispec.MediaTypeImageIndex:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *dockerFetcher) setTokenAuth(ctx context.Context, params map[string]string) error {
|
||||
realm, ok := params["realm"]
|
||||
if !ok {
|
||||
return errors.New("no realm specified for token auth challenge")
|
||||
}
|
||||
|
||||
realmURL, err := url.Parse(realm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
return "", errors.Errorf("unexpected status code: %v %v", resp.StatusCode, resp.Status)
|
||||
return fmt.Errorf("invalid token auth challenge realm: %s", err)
|
||||
}
|
||||
|
||||
p, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
to := tokenOptions{
|
||||
realm: realmURL.String(),
|
||||
service: params["service"],
|
||||
}
|
||||
|
||||
var tokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
scope, ok := params["scope"]
|
||||
if !ok {
|
||||
return errors.Errorf("no scope specified for token auth challenge")
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(p, &tokenResponse); err != nil {
|
||||
return "", err
|
||||
// TODO: Get added scopes from context
|
||||
to.scopes = []string{scope}
|
||||
|
||||
if r.secret != "" {
|
||||
// Credential information is provided, use oauth POST endpoint
|
||||
r.token, err = r.fetchTokenWithOAuth(ctx, to)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to fetch oauth token")
|
||||
}
|
||||
} else {
|
||||
// Do request anonymously
|
||||
r.token, err = r.getToken(ctx, to)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to fetch anonymous token")
|
||||
}
|
||||
}
|
||||
|
||||
return tokenResponse.Token, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// getV2URLPaths generates the candidate urls paths for the object based on the
|
||||
@ -291,3 +404,125 @@ func getV2URLPaths(desc ocispec.Descriptor) ([]string, error) {
|
||||
|
||||
return urls, nil
|
||||
}
|
||||
|
||||
type tokenOptions struct {
|
||||
realm string
|
||||
service string
|
||||
scopes []string
|
||||
}
|
||||
|
||||
type postTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
func (r *dockerFetcher) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
|
||||
form := url.Values{}
|
||||
form.Set("scope", strings.Join(to.scopes, " "))
|
||||
form.Set("service", to.service)
|
||||
// TODO: Allow setting client_id
|
||||
form.Set("client_id", "containerd-dist-tool")
|
||||
|
||||
if r.username == "" {
|
||||
form.Set("grant_type", "refresh_token")
|
||||
form.Set("refresh_token", r.secret)
|
||||
} else {
|
||||
form.Set("grant_type", "password")
|
||||
form.Set("username", r.username)
|
||||
form.Set("password", r.secret)
|
||||
}
|
||||
|
||||
resp, err := ctxhttp.PostForm(ctx, r.client, to.realm, form)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 405 && r.username != "" {
|
||||
// It would be nice if registries would implement the specifications
|
||||
return r.getToken(ctx, to)
|
||||
} else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
log.G(ctx).WithFields(logrus.Fields{
|
||||
"status": resp.Status,
|
||||
"body": string(b),
|
||||
}).Debugf("token request failed")
|
||||
// TODO: handle error body and write debug output
|
||||
return "", errors.Errorf("unexpected status: %s", resp.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
var tr postTokenResponse
|
||||
if err = decoder.Decode(&tr); err != nil {
|
||||
return "", fmt.Errorf("unable to decode token response: %s", err)
|
||||
}
|
||||
|
||||
return tr.AccessToken, nil
|
||||
}
|
||||
|
||||
type getTokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// getToken fetches a token using a GET request
|
||||
func (r *dockerFetcher) getToken(ctx context.Context, to tokenOptions) (string, error) {
|
||||
req, err := http.NewRequest("GET", to.realm, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
reqParams := req.URL.Query()
|
||||
|
||||
if to.service != "" {
|
||||
reqParams.Add("service", to.service)
|
||||
}
|
||||
|
||||
for _, scope := range to.scopes {
|
||||
reqParams.Add("scope", scope)
|
||||
}
|
||||
|
||||
if r.secret != "" {
|
||||
req.SetBasicAuth(r.username, r.secret)
|
||||
}
|
||||
|
||||
req.URL.RawQuery = reqParams.Encode()
|
||||
|
||||
resp, err := ctxhttp.Do(ctx, r.client, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||
// TODO: handle error body and write debug output
|
||||
return "", errors.Errorf("unexpected status: %s", resp.Status)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
var tr getTokenResponse
|
||||
if err = decoder.Decode(&tr); err != nil {
|
||||
return "", fmt.Errorf("unable to decode token response: %s", err)
|
||||
}
|
||||
|
||||
// `access_token` is equivalent to `token` and if both are specified
|
||||
// the choice is undefined. Canonicalize `access_token` by sticking
|
||||
// things in `token`.
|
||||
if tr.AccessToken != "" {
|
||||
tr.Token = tr.AccessToken
|
||||
}
|
||||
|
||||
if tr.Token == "" {
|
||||
return "", ErrNoToken
|
||||
}
|
||||
|
||||
return tr.Token, nil
|
||||
}
|
||||
|
388
remotes/docker/resolver_test.go
Normal file
388
remotes/docker/resolver_test.go
Normal file
@ -0,0 +1,388 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/remotes"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
specs "github.com/opencontainers/image-spec/specs-go"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestHTTPResolver(t *testing.T) {
|
||||
s := func(h http.Handler) (string, ResolverOptions, func()) {
|
||||
s := httptest.NewServer(h)
|
||||
|
||||
options := ResolverOptions{
|
||||
PlainHTTP: true,
|
||||
}
|
||||
base := s.URL[7:] // strip "http://"
|
||||
return base, options, s.Close
|
||||
}
|
||||
|
||||
runBasicTest(t, "testname", s)
|
||||
}
|
||||
|
||||
func TestHTTPSResolver(t *testing.T) {
|
||||
runBasicTest(t, "testname", tlsServer)
|
||||
}
|
||||
|
||||
func TestBasicResolver(t *testing.T) {
|
||||
basicAuth := func(h http.Handler) (string, ResolverOptions, func()) {
|
||||
// Wrap with basic auth
|
||||
wrapped := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok || username != "user1" || password != "password1" {
|
||||
rw.Header().Set("WWW-Authenticate", "Basic realm=localhost")
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(rw, r)
|
||||
})
|
||||
|
||||
base, options, close := tlsServer(wrapped)
|
||||
options.Credentials = func(string) (string, string, error) {
|
||||
return "user1", "password1", nil
|
||||
}
|
||||
return base, options, close
|
||||
}
|
||||
runBasicTest(t, "testname", basicAuth)
|
||||
}
|
||||
|
||||
func TestAnonymousTokenResolver(t *testing.T) {
|
||||
th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`))
|
||||
})
|
||||
|
||||
runBasicTest(t, "testname", withTokenServer(th, nil))
|
||||
}
|
||||
|
||||
func TestBasicAuthTokenResolver(t *testing.T) {
|
||||
th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok || username != "user1" || password != "password1" {
|
||||
rw.Write([]byte(`{"access_token":"insufficientscope"}`))
|
||||
} else {
|
||||
rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`))
|
||||
}
|
||||
})
|
||||
creds := func(string) (string, string, error) {
|
||||
return "user1", "password1", nil
|
||||
}
|
||||
|
||||
runBasicTest(t, "testname", withTokenServer(th, creds))
|
||||
}
|
||||
|
||||
func TestRefreshTokenResolver(t *testing.T) {
|
||||
th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
|
||||
r.ParseForm()
|
||||
if r.PostForm.Get("grant_type") != "refresh_token" || r.PostForm.Get("refresh_token") != "somerefreshtoken" {
|
||||
rw.Write([]byte(`{"access_token":"insufficientscope"}`))
|
||||
} else {
|
||||
rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`))
|
||||
}
|
||||
})
|
||||
creds := func(string) (string, string, error) {
|
||||
return "", "somerefreshtoken", nil
|
||||
}
|
||||
|
||||
runBasicTest(t, "testname", withTokenServer(th, creds))
|
||||
}
|
||||
|
||||
func TestPostBasicAuthTokenResolver(t *testing.T) {
|
||||
th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
|
||||
r.ParseForm()
|
||||
if r.PostForm.Get("grant_type") != "password" || r.PostForm.Get("username") != "user1" || r.PostForm.Get("password") != "password1" {
|
||||
rw.Write([]byte(`{"access_token":"insufficientscope"}`))
|
||||
} else {
|
||||
rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`))
|
||||
}
|
||||
})
|
||||
creds := func(string) (string, string, error) {
|
||||
return "user1", "password1", nil
|
||||
}
|
||||
|
||||
runBasicTest(t, "testname", withTokenServer(th, creds))
|
||||
}
|
||||
|
||||
func TestBadTokenResolver(t *testing.T) {
|
||||
th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte(`{"access_token":"insufficientscope"}`))
|
||||
})
|
||||
creds := func(string) (string, string, error) {
|
||||
return "", "somerefreshtoken", nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
h := content(ocispec.MediaTypeImageManifest, []byte("not anything parse-able"))
|
||||
|
||||
base, ro, close := withTokenServer(th, creds)(logHandler{t, h})
|
||||
defer close()
|
||||
|
||||
resolver := NewResolver(ro)
|
||||
image := fmt.Sprintf("%s/doesntmatter:sometatg", base)
|
||||
|
||||
_, _, _, err := resolver.Resolve(ctx, image)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error getting token with inssufficient scope")
|
||||
}
|
||||
if errors.Cause(err) != ErrInvalidAuthorization {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func withTokenServer(th http.Handler, creds func(string) (string, string, error)) func(h http.Handler) (string, ResolverOptions, func()) {
|
||||
return func(h http.Handler) (string, ResolverOptions, func()) {
|
||||
s := httptest.NewUnstartedServer(th)
|
||||
s.StartTLS()
|
||||
|
||||
cert, _ := x509.ParseCertificate(s.TLS.Certificates[0].Certificate[0])
|
||||
tokenBase := s.URL + "/token"
|
||||
|
||||
// Wrap with token auth
|
||||
wrapped := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
auth := strings.ToLower(r.Header.Get("Authorization"))
|
||||
if auth != "bearer perfectlyvalidopaquetoken" {
|
||||
authHeader := fmt.Sprintf("Bearer realm=%q,service=registry,scope=\"repository:testname:pull,pull\"", tokenBase)
|
||||
if strings.HasPrefix(auth, "bearer ") {
|
||||
authHeader = authHeader + ",error=" + auth[7:]
|
||||
}
|
||||
rw.Header().Set("WWW-Authenticate", authHeader)
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(rw, r)
|
||||
})
|
||||
|
||||
base, options, close := tlsServer(wrapped)
|
||||
options.Credentials = creds
|
||||
options.Client.Transport.(*http.Transport).TLSClientConfig.RootCAs.AddCert(cert)
|
||||
return base, options, func() {
|
||||
s.Close()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tlsServer(h http.Handler) (string, ResolverOptions, func()) {
|
||||
s := httptest.NewUnstartedServer(h)
|
||||
s.StartTLS()
|
||||
|
||||
capool := x509.NewCertPool()
|
||||
cert, _ := x509.ParseCertificate(s.TLS.Certificates[0].Certificate[0])
|
||||
capool.AddCert(cert)
|
||||
|
||||
options := ResolverOptions{
|
||||
Client: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: capool,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
base := s.URL[8:] // strip "https://"
|
||||
return base, options, s.Close
|
||||
}
|
||||
|
||||
type logHandler struct {
|
||||
t *testing.T
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (h logHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
h.t.Logf("%s %s", r.Method, r.URL.String())
|
||||
h.handler.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
func runBasicTest(t *testing.T, name string, sf func(h http.Handler) (string, ResolverOptions, func())) {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
tag = "latest"
|
||||
r = http.NewServeMux()
|
||||
)
|
||||
|
||||
m := newManifest(
|
||||
content(ocispec.MediaTypeImageConfig, []byte("1")),
|
||||
content(ocispec.MediaTypeImageLayerGzip, []byte("2")),
|
||||
)
|
||||
mc := content(ocispec.MediaTypeImageManifest, m.OCIManifest())
|
||||
m.RegisterHandler(r, name)
|
||||
r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, tag), mc)
|
||||
r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, mc.Digest()), mc)
|
||||
|
||||
base, ro, close := sf(logHandler{t, r})
|
||||
defer close()
|
||||
|
||||
resolver := NewResolver(ro)
|
||||
image := fmt.Sprintf("%s/%s:%s", base, name, tag)
|
||||
|
||||
_, d, f, err := resolver.Resolve(ctx, image)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
refs, err := testocimanifest(ctx, f, d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(refs) != 2 {
|
||||
t.Fatalf("Unexpected number of references: %d, expected 2", len(refs))
|
||||
}
|
||||
|
||||
for _, ref := range refs {
|
||||
if err := testFetch(ctx, f, ref); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testFetch(ctx context.Context, f remotes.Fetcher, desc ocispec.Descriptor) error {
|
||||
r, err := f.Fetch(ctx, desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dgstr := desc.Digest.Algorithm().Digester()
|
||||
io.Copy(dgstr.Hash(), r)
|
||||
if dgstr.Digest() != desc.Digest {
|
||||
return errors.Errorf("content mismatch: %s != %s", dgstr.Digest(), desc.Digest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func testocimanifest(ctx context.Context, f remotes.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
r, err := f.Fetch(ctx, desc)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to fetch %s", desc.Digest)
|
||||
}
|
||||
p, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dgst := desc.Digest.Algorithm().FromBytes(p); dgst != desc.Digest {
|
||||
return nil, errors.Errorf("digest mismatch: %s != %s", dgst, desc.Digest)
|
||||
}
|
||||
|
||||
var manifest ocispec.Manifest
|
||||
if err := json.Unmarshal(p, &manifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var descs []ocispec.Descriptor
|
||||
|
||||
descs = append(descs, manifest.Config)
|
||||
descs = append(descs, manifest.Layers...)
|
||||
|
||||
return descs, nil
|
||||
}
|
||||
|
||||
type testContent struct {
|
||||
mediaType string
|
||||
content []byte
|
||||
}
|
||||
|
||||
func content(mediaType string, b []byte) testContent {
|
||||
return testContent{
|
||||
mediaType: mediaType,
|
||||
content: b,
|
||||
}
|
||||
}
|
||||
|
||||
func (tc testContent) Descriptor() ocispec.Descriptor {
|
||||
return ocispec.Descriptor{
|
||||
MediaType: tc.mediaType,
|
||||
Digest: digest.FromBytes(tc.content),
|
||||
Size: int64(len(tc.content)),
|
||||
}
|
||||
}
|
||||
|
||||
func (tc testContent) Digest() digest.Digest {
|
||||
return digest.FromBytes(tc.content)
|
||||
}
|
||||
|
||||
func (tc testContent) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", tc.mediaType)
|
||||
w.Header().Add("Content-Length", strconv.Itoa(len(tc.content)))
|
||||
w.Header().Add("Docker-Content-Digest", tc.Digest().String())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(tc.content)
|
||||
}
|
||||
|
||||
type testManifest struct {
|
||||
config testContent
|
||||
references []testContent
|
||||
}
|
||||
|
||||
func newManifest(config testContent, refs ...testContent) testManifest {
|
||||
return testManifest{
|
||||
config: config,
|
||||
references: refs,
|
||||
}
|
||||
}
|
||||
|
||||
func (m testManifest) OCIManifest() []byte {
|
||||
manifest := ocispec.Manifest{
|
||||
Versioned: specs.Versioned{
|
||||
SchemaVersion: 1,
|
||||
},
|
||||
Config: m.config.Descriptor(),
|
||||
Layers: make([]ocispec.Descriptor, len(m.references)),
|
||||
}
|
||||
for i, c := range append(m.references) {
|
||||
manifest.Layers[i] = c.Descriptor()
|
||||
}
|
||||
b, _ := json.Marshal(manifest)
|
||||
return b
|
||||
}
|
||||
|
||||
func (m testManifest) RegisterHandler(r *http.ServeMux, name string) {
|
||||
for _, c := range append(m.references, m.config) {
|
||||
r.Handle(fmt.Sprintf("/v2/%s/blobs/%s", name, c.Digest()), c)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
github.com/coreos/go-systemd 48702e0da86bd25e76cfef347e2adeb434a0d0a6
|
||||
github.com/crosbymichael/go-runc 65847bfc51952703ca24b564d10de50d3f2db6e7
|
||||
github.com/crosbymichael/console f13f890e20a94bdec6c328cdf9410b7158f0cfa4
|
||||
github.com/crosbymichael/console 2a5cbd32a84cd1268c20c69bd090ec49e37009f8
|
||||
github.com/crosbymichael/cgroups e950a27f3faf567abbf995bfbec90eaddc766d25
|
||||
github.com/docker/go-metrics 8fd5772bf1584597834c6f7961a530f06cbfbb87
|
||||
github.com/godbus/dbus c7fdd8b5cd55e87b4e1f4e372cdb1db61dd6c66f
|
||||
|
2
vendor/github.com/crosbymichael/console/console.go
generated
vendored
2
vendor/github.com/crosbymichael/console/console.go
generated
vendored
@ -20,6 +20,8 @@ type Console interface {
|
||||
ResizeFrom(Console) error
|
||||
// SetRaw sets the console in raw mode
|
||||
SetRaw() error
|
||||
// DisableEcho disables echo on the console
|
||||
DisableEcho() error
|
||||
// Reset restores the console to its orignal state
|
||||
Reset() error
|
||||
// Size returns the window size of the console
|
||||
|
@ -1,3 +1,5 @@
|
||||
// +build darwin freebsd linux
|
||||
|
||||
package console
|
||||
|
||||
// #include <termios.h>
|
||||
@ -34,8 +36,8 @@ func NewPty() (Console, string, error) {
|
||||
}
|
||||
|
||||
type master struct {
|
||||
f *os.File
|
||||
termios *unix.Termios
|
||||
f *os.File
|
||||
original *unix.Termios
|
||||
}
|
||||
|
||||
func (m *master) Read(b []byte) (int, error) {
|
||||
@ -67,23 +69,42 @@ func (m *master) ResizeFrom(c Console) error {
|
||||
}
|
||||
|
||||
func (m *master) Reset() error {
|
||||
if m.termios == nil {
|
||||
if m.original == nil {
|
||||
return nil
|
||||
}
|
||||
return tcset(m.f.Fd(), m.termios)
|
||||
return tcset(m.f.Fd(), m.original)
|
||||
}
|
||||
|
||||
func (m *master) getCurrent() (unix.Termios, error) {
|
||||
var termios unix.Termios
|
||||
if err := tcget(m.f.Fd(), &termios); err != nil {
|
||||
return unix.Termios{}, err
|
||||
}
|
||||
if m.original == nil {
|
||||
m.original = &termios
|
||||
}
|
||||
return termios, nil
|
||||
}
|
||||
|
||||
func (m *master) SetRaw() error {
|
||||
m.termios = &unix.Termios{}
|
||||
if err := tcget(m.f.Fd(), m.termios); err != nil {
|
||||
rawState, err := m.getCurrent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawState := *m.termios
|
||||
C.cfmakeraw((*C.struct_termios)(unsafe.Pointer(&rawState)))
|
||||
rawState.Oflag = rawState.Oflag | C.OPOST
|
||||
return tcset(m.f.Fd(), &rawState)
|
||||
}
|
||||
|
||||
func (m *master) DisableEcho() error {
|
||||
rawState, err := m.getCurrent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawState.Lflag = rawState.Lflag &^ unix.ECHO
|
||||
return tcset(m.f.Fd(), &rawState)
|
||||
}
|
||||
|
||||
func (m *master) Size() (WinSize, error) {
|
||||
var ws WinSize
|
||||
if err := ioctl(
|
12
vendor/github.com/crosbymichael/console/console_windows.go
generated
vendored
12
vendor/github.com/crosbymichael/console/console_windows.go
generated
vendored
@ -126,6 +126,18 @@ func (m *master) ResizeFrom(c Console) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func (m *master) DisableEcho() error {
|
||||
mode := m.inMode &^ winterm.ENABLE_ECHO_INPUT
|
||||
mode |= winterm.ENABLE_PROCESSED_INPUT
|
||||
mode |= winterm.ENABLE_LINE_INPUT
|
||||
|
||||
if err := winterm.SetConsoleMode(m.in, mode); err != nil {
|
||||
return errors.Wrap(err, "unable to set console to disable echo")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *master) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
51
vendor/github.com/crosbymichael/console/tc_darwin.go
generated
vendored
Normal file
51
vendor/github.com/crosbymichael/console/tc_darwin.go
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func tcget(fd uintptr, p *unix.Termios) error {
|
||||
return ioctl(fd, unix.TIOCGETA, uintptr(unsafe.Pointer(p)))
|
||||
}
|
||||
|
||||
func tcset(fd uintptr, p *unix.Termios) error {
|
||||
return ioctl(fd, unix.TIOCSETA, uintptr(unsafe.Pointer(p)))
|
||||
}
|
||||
|
||||
func ioctl(fd, flag, data uintptr) error {
|
||||
if _, _, err := unix.Syscall(unix.SYS_IOCTL, fd, flag, data); err != 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f.
|
||||
// unlockpt should be called before opening the slave side of a pty.
|
||||
func unlockpt(f *os.File) error {
|
||||
var u int32
|
||||
return ioctl(f.Fd(), unix.TIOCPTYUNLK, uintptr(unsafe.Pointer(&u)))
|
||||
}
|
||||
|
||||
// ptsname retrieves the name of the first available pts for the given master.
|
||||
func ptsname(f *os.File) (string, error) {
|
||||
var n int32
|
||||
if err := ioctl(f.Fd(), unix.TIOCPTYGNAME, uintptr(unsafe.Pointer(&n))); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("/dev/pts/%d", n), nil
|
||||
}
|
||||
|
||||
func saneTerminal(f *os.File) error {
|
||||
// Go doesn't have a wrapper for any of the termios ioctls.
|
||||
var termios unix.Termios
|
||||
if err := tcget(f.Fd(), &termios); err != nil {
|
||||
return err
|
||||
}
|
||||
// Set -onlcr so we don't have to deal with \r.
|
||||
termios.Oflag &^= unix.ONLCR
|
||||
return tcset(f.Fd(), &termios)
|
||||
}
|
51
vendor/github.com/crosbymichael/console/tc_freebsd.go
generated
vendored
Normal file
51
vendor/github.com/crosbymichael/console/tc_freebsd.go
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func tcget(fd uintptr, p *unix.Termios) error {
|
||||
return ioctl(fd, unix.TIOCGETA, uintptr(unsafe.Pointer(p)))
|
||||
}
|
||||
|
||||
func tcset(fd uintptr, p *unix.Termios) error {
|
||||
return ioctl(fd, unix.TIOCSETA, uintptr(unsafe.Pointer(p)))
|
||||
}
|
||||
|
||||
func ioctl(fd, flag, data uintptr) error {
|
||||
if _, _, err := unix.Syscall(unix.SYS_IOCTL, fd, flag, data); err != 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f.
|
||||
// unlockpt should be called before opening the slave side of a pty.
|
||||
// This does not exist on FreeBSD, it does not allocate controlling terminals on open
|
||||
func unlockpt(f *os.File) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ptsname retrieves the name of the first available pts for the given master.
|
||||
func ptsname(f *os.File) (string, error) {
|
||||
var n int32
|
||||
if err := ioctl(f.Fd(), unix.TIOCGPTN, uintptr(unsafe.Pointer(&n))); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("/dev/pts/%d", n), nil
|
||||
}
|
||||
|
||||
func saneTerminal(f *os.File) error {
|
||||
// Go doesn't have a wrapper for any of the termios ioctls.
|
||||
var termios unix.Termios
|
||||
if err := tcget(f.Fd(), &termios); err != nil {
|
||||
return err
|
||||
}
|
||||
// Set -onlcr so we don't have to deal with \r.
|
||||
termios.Oflag &^= unix.ONLCR
|
||||
return tcset(f.Fd(), &termios)
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
// +build linux
|
||||
|
||||
package console
|
||||
|
||||
import (
|
Loading…
Reference in New Issue
Block a user