github.com/containerd/Containerd@v1.4.13/remotes/docker/resolver.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package docker
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"net/http"
    25  	"net/url"
    26  	"path"
    27  	"strings"
    28  
    29  	"github.com/containerd/containerd/errdefs"
    30  	"github.com/containerd/containerd/images"
    31  	"github.com/containerd/containerd/log"
    32  	"github.com/containerd/containerd/reference"
    33  	"github.com/containerd/containerd/remotes"
    34  	"github.com/containerd/containerd/remotes/docker/schema1"
    35  	"github.com/containerd/containerd/version"
    36  	digest "github.com/opencontainers/go-digest"
    37  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    38  	"github.com/pkg/errors"
    39  	"github.com/sirupsen/logrus"
    40  	"golang.org/x/net/context/ctxhttp"
    41  )
    42  
    43  var (
    44  	// ErrNoToken is returned if a request is successful but the body does not
    45  	// contain an authorization token.
    46  	ErrNoToken = errors.New("authorization server did not include a token in the response")
    47  
    48  	// ErrInvalidAuthorization is used when credentials are passed to a server but
    49  	// those credentials are rejected.
    50  	ErrInvalidAuthorization = errors.New("authorization failed")
    51  
    52  	// MaxManifestSize represents the largest size accepted from a registry
    53  	// during resolution. Larger manifests may be accepted using a
    54  	// resolution method other than the registry.
    55  	//
    56  	// NOTE: The max supported layers by some runtimes is 128 and individual
    57  	// layers will not contribute more than 256 bytes, making a
    58  	// reasonable limit for a large image manifests of 32K bytes.
    59  	// 4M bytes represents a much larger upper bound for images which may
    60  	// contain large annotations or be non-images. A proper manifest
    61  	// design puts large metadata in subobjects, as is consistent the
    62  	// intent of the manifest design.
    63  	MaxManifestSize int64 = 4 * 1048 * 1048
    64  )
    65  
    66  // Authorizer is used to authorize HTTP requests based on 401 HTTP responses.
    67  // An Authorizer is responsible for caching tokens or credentials used by
    68  // requests.
    69  type Authorizer interface {
    70  	// Authorize sets the appropriate `Authorization` header on the given
    71  	// request.
    72  	//
    73  	// If no authorization is found for the request, the request remains
    74  	// unmodified. It may also add an `Authorization` header as
    75  	//  "bearer <some bearer token>"
    76  	//  "basic <base64 encoded credentials>"
    77  	Authorize(context.Context, *http.Request) error
    78  
    79  	// AddResponses adds a 401 response for the authorizer to consider when
    80  	// authorizing requests. The last response should be unauthorized and
    81  	// the previous requests are used to consider redirects and retries
    82  	// that may have led to the 401.
    83  	//
    84  	// If response is not handled, returns `ErrNotImplemented`
    85  	AddResponses(context.Context, []*http.Response) error
    86  }
    87  
    88  // ResolverOptions are used to configured a new Docker register resolver
    89  type ResolverOptions struct {
    90  	// Hosts returns registry host configurations for a namespace.
    91  	Hosts RegistryHosts
    92  
    93  	// Headers are the HTTP request header fields sent by the resolver
    94  	Headers http.Header
    95  
    96  	// Tracker is used to track uploads to the registry. This is used
    97  	// since the registry does not have upload tracking and the existing
    98  	// mechanism for getting blob upload status is expensive.
    99  	Tracker StatusTracker
   100  
   101  	// Authorizer is used to authorize registry requests
   102  	// Deprecated: use Hosts
   103  	Authorizer Authorizer
   104  
   105  	// Credentials provides username and secret given a host.
   106  	// If username is empty but a secret is given, that secret
   107  	// is interpreted as a long lived token.
   108  	// Deprecated: use Hosts
   109  	Credentials func(string) (string, string, error)
   110  
   111  	// Host provides the hostname given a namespace.
   112  	// Deprecated: use Hosts
   113  	Host func(string) (string, error)
   114  
   115  	// PlainHTTP specifies to use plain http and not https
   116  	// Deprecated: use Hosts
   117  	PlainHTTP bool
   118  
   119  	// Client is the http client to used when making registry requests
   120  	// Deprecated: use Hosts
   121  	Client *http.Client
   122  }
   123  
   124  // DefaultHost is the default host function.
   125  func DefaultHost(ns string) (string, error) {
   126  	if ns == "docker.io" {
   127  		return "registry-1.docker.io", nil
   128  	}
   129  	return ns, nil
   130  }
   131  
   132  type dockerResolver struct {
   133  	hosts         RegistryHosts
   134  	header        http.Header
   135  	resolveHeader http.Header
   136  	tracker       StatusTracker
   137  }
   138  
   139  // NewResolver returns a new resolver to a Docker registry
   140  func NewResolver(options ResolverOptions) remotes.Resolver {
   141  	if options.Tracker == nil {
   142  		options.Tracker = NewInMemoryTracker()
   143  	}
   144  
   145  	if options.Headers == nil {
   146  		options.Headers = make(http.Header)
   147  	}
   148  	if _, ok := options.Headers["User-Agent"]; !ok {
   149  		options.Headers.Set("User-Agent", "containerd/"+version.Version)
   150  	}
   151  
   152  	resolveHeader := http.Header{}
   153  	if _, ok := options.Headers["Accept"]; !ok {
   154  		// set headers for all the types we support for resolution.
   155  		resolveHeader.Set("Accept", strings.Join([]string{
   156  			images.MediaTypeDockerSchema2Manifest,
   157  			images.MediaTypeDockerSchema2ManifestList,
   158  			ocispec.MediaTypeImageManifest,
   159  			ocispec.MediaTypeImageIndex, "*/*"}, ", "))
   160  	} else {
   161  		resolveHeader["Accept"] = options.Headers["Accept"]
   162  		delete(options.Headers, "Accept")
   163  	}
   164  
   165  	if options.Hosts == nil {
   166  		opts := []RegistryOpt{}
   167  		if options.Host != nil {
   168  			opts = append(opts, WithHostTranslator(options.Host))
   169  		}
   170  
   171  		if options.Authorizer == nil {
   172  			options.Authorizer = NewDockerAuthorizer(
   173  				WithAuthClient(options.Client),
   174  				WithAuthHeader(options.Headers),
   175  				WithAuthCreds(options.Credentials))
   176  		}
   177  		opts = append(opts, WithAuthorizer(options.Authorizer))
   178  
   179  		if options.Client != nil {
   180  			opts = append(opts, WithClient(options.Client))
   181  		}
   182  		if options.PlainHTTP {
   183  			opts = append(opts, WithPlainHTTP(MatchAllHosts))
   184  		} else {
   185  			opts = append(opts, WithPlainHTTP(MatchLocalhost))
   186  		}
   187  		options.Hosts = ConfigureDefaultRegistries(opts...)
   188  	}
   189  	return &dockerResolver{
   190  		hosts:         options.Hosts,
   191  		header:        options.Headers,
   192  		resolveHeader: resolveHeader,
   193  		tracker:       options.Tracker,
   194  	}
   195  }
   196  
   197  func getManifestMediaType(resp *http.Response) string {
   198  	// Strip encoding data (manifests should always be ascii JSON)
   199  	contentType := resp.Header.Get("Content-Type")
   200  	if sp := strings.IndexByte(contentType, ';'); sp != -1 {
   201  		contentType = contentType[0:sp]
   202  	}
   203  
   204  	// As of Apr 30 2019 the registry.access.redhat.com registry does not specify
   205  	// the content type of any data but uses schema1 manifests.
   206  	if contentType == "text/plain" {
   207  		contentType = images.MediaTypeDockerSchema1Manifest
   208  	}
   209  	return contentType
   210  }
   211  
   212  type countingReader struct {
   213  	reader    io.Reader
   214  	bytesRead int64
   215  }
   216  
   217  func (r *countingReader) Read(p []byte) (int, error) {
   218  	n, err := r.reader.Read(p)
   219  	r.bytesRead += int64(n)
   220  	return n, err
   221  }
   222  
   223  var _ remotes.Resolver = &dockerResolver{}
   224  
   225  func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocispec.Descriptor, error) {
   226  	refspec, err := reference.Parse(ref)
   227  	if err != nil {
   228  		return "", ocispec.Descriptor{}, err
   229  	}
   230  
   231  	if refspec.Object == "" {
   232  		return "", ocispec.Descriptor{}, reference.ErrObjectRequired
   233  	}
   234  
   235  	base, err := r.base(refspec)
   236  	if err != nil {
   237  		return "", ocispec.Descriptor{}, err
   238  	}
   239  
   240  	var (
   241  		firstErr error
   242  		paths    [][]string
   243  		dgst     = refspec.Digest()
   244  		caps     = HostCapabilityPull
   245  	)
   246  
   247  	if dgst != "" {
   248  		if err := dgst.Validate(); err != nil {
   249  			// need to fail here, since we can't actually resolve the invalid
   250  			// digest.
   251  			return "", ocispec.Descriptor{}, err
   252  		}
   253  
   254  		// turns out, we have a valid digest, make a url.
   255  		paths = append(paths, []string{"manifests", dgst.String()})
   256  
   257  		// fallback to blobs on not found.
   258  		paths = append(paths, []string{"blobs", dgst.String()})
   259  	} else {
   260  		// Add
   261  		paths = append(paths, []string{"manifests", refspec.Object})
   262  		caps |= HostCapabilityResolve
   263  	}
   264  
   265  	hosts := base.filterHosts(caps)
   266  	if len(hosts) == 0 {
   267  		return "", ocispec.Descriptor{}, errors.Wrap(errdefs.ErrNotFound, "no resolve hosts")
   268  	}
   269  
   270  	ctx, err = contextWithRepositoryScope(ctx, refspec, false)
   271  	if err != nil {
   272  		return "", ocispec.Descriptor{}, err
   273  	}
   274  
   275  	for _, u := range paths {
   276  		for _, host := range hosts {
   277  			ctx := log.WithLogger(ctx, log.G(ctx).WithField("host", host.Host))
   278  
   279  			req := base.request(host, http.MethodHead, u...)
   280  			if err := req.addNamespace(base.refspec.Hostname()); err != nil {
   281  				return "", ocispec.Descriptor{}, err
   282  			}
   283  
   284  			for key, value := range r.resolveHeader {
   285  				req.header[key] = append(req.header[key], value...)
   286  			}
   287  
   288  			log.G(ctx).Debug("resolving")
   289  			resp, err := req.doWithRetries(ctx, nil)
   290  			if err != nil {
   291  				if errors.Is(err, ErrInvalidAuthorization) {
   292  					err = errors.Wrapf(err, "pull access denied, repository does not exist or may require authorization")
   293  				}
   294  				// Store the error for referencing later
   295  				if firstErr == nil {
   296  					firstErr = err
   297  				}
   298  				log.G(ctx).WithError(err).Info("trying next host")
   299  				continue // try another host
   300  			}
   301  			resp.Body.Close() // don't care about body contents.
   302  
   303  			if resp.StatusCode > 299 {
   304  				if resp.StatusCode == http.StatusNotFound {
   305  					log.G(ctx).Info("trying next host - response was http.StatusNotFound")
   306  					continue
   307  				}
   308  				if resp.StatusCode > 399 {
   309  					// Set firstErr when encountering the first non-404 status code.
   310  					if firstErr == nil {
   311  						firstErr = errors.Errorf("pulling from host %s failed with status code %v: %v", host.Host, u, resp.Status)
   312  					}
   313  					continue // try another host
   314  				}
   315  				return "", ocispec.Descriptor{}, errors.Errorf("pulling from host %s failed with unexpected status code %v: %v", host.Host, u, resp.Status)
   316  			}
   317  			size := resp.ContentLength
   318  			contentType := getManifestMediaType(resp)
   319  
   320  			// if no digest was provided, then only a resolve
   321  			// trusted registry was contacted, in this case use
   322  			// the digest header (or content from GET)
   323  			if dgst == "" {
   324  				// this is the only point at which we trust the registry. we use the
   325  				// content headers to assemble a descriptor for the name. when this becomes
   326  				// more robust, we mostly get this information from a secure trust store.
   327  				dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest"))
   328  
   329  				if dgstHeader != "" && size != -1 {
   330  					if err := dgstHeader.Validate(); err != nil {
   331  						return "", ocispec.Descriptor{}, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader)
   332  					}
   333  					dgst = dgstHeader
   334  				}
   335  			}
   336  			if dgst == "" || size == -1 {
   337  				log.G(ctx).Debug("no Docker-Content-Digest header, fetching manifest instead")
   338  
   339  				req = base.request(host, http.MethodGet, u...)
   340  				if err := req.addNamespace(base.refspec.Hostname()); err != nil {
   341  					return "", ocispec.Descriptor{}, err
   342  				}
   343  
   344  				for key, value := range r.resolveHeader {
   345  					req.header[key] = append(req.header[key], value...)
   346  				}
   347  
   348  				resp, err := req.doWithRetries(ctx, nil)
   349  				if err != nil {
   350  					return "", ocispec.Descriptor{}, err
   351  				}
   352  				defer resp.Body.Close()
   353  
   354  				bodyReader := countingReader{reader: resp.Body}
   355  
   356  				contentType = getManifestMediaType(resp)
   357  				if dgst == "" {
   358  					if contentType == images.MediaTypeDockerSchema1Manifest {
   359  						b, err := schema1.ReadStripSignature(&bodyReader)
   360  						if err != nil {
   361  							return "", ocispec.Descriptor{}, err
   362  						}
   363  
   364  						dgst = digest.FromBytes(b)
   365  					} else {
   366  						dgst, err = digest.FromReader(&bodyReader)
   367  						if err != nil {
   368  							return "", ocispec.Descriptor{}, err
   369  						}
   370  					}
   371  				} else if _, err := io.Copy(ioutil.Discard, &bodyReader); err != nil {
   372  					return "", ocispec.Descriptor{}, err
   373  				}
   374  				size = bodyReader.bytesRead
   375  			}
   376  			// Prevent resolving to excessively large manifests
   377  			if size > MaxManifestSize {
   378  				if firstErr == nil {
   379  					firstErr = errors.Wrapf(errdefs.ErrNotFound, "rejecting %d byte manifest for %s", size, ref)
   380  				}
   381  				continue
   382  			}
   383  
   384  			desc := ocispec.Descriptor{
   385  				Digest:    dgst,
   386  				MediaType: contentType,
   387  				Size:      size,
   388  			}
   389  
   390  			log.G(ctx).WithField("desc.digest", desc.Digest).Debug("resolved")
   391  			return ref, desc, nil
   392  		}
   393  	}
   394  
   395  	// If above loop terminates without return, then there was an error.
   396  	// "firstErr" contains the first non-404 error. That is, "firstErr == nil"
   397  	// means that either no registries were given or each registry returned 404.
   398  
   399  	if firstErr == nil {
   400  		firstErr = errors.Wrap(errdefs.ErrNotFound, ref)
   401  	}
   402  
   403  	return "", ocispec.Descriptor{}, firstErr
   404  }
   405  
   406  func (r *dockerResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
   407  	refspec, err := reference.Parse(ref)
   408  	if err != nil {
   409  		return nil, err
   410  	}
   411  
   412  	base, err := r.base(refspec)
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	return dockerFetcher{
   418  		dockerBase: base,
   419  	}, nil
   420  }
   421  
   422  func (r *dockerResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) {
   423  	refspec, err := reference.Parse(ref)
   424  	if err != nil {
   425  		return nil, err
   426  	}
   427  
   428  	base, err := r.base(refspec)
   429  	if err != nil {
   430  		return nil, err
   431  	}
   432  
   433  	return dockerPusher{
   434  		dockerBase: base,
   435  		object:     refspec.Object,
   436  		tracker:    r.tracker,
   437  	}, nil
   438  }
   439  
   440  type dockerBase struct {
   441  	refspec    reference.Spec
   442  	repository string
   443  	hosts      []RegistryHost
   444  	header     http.Header
   445  }
   446  
   447  func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
   448  	host := refspec.Hostname()
   449  	hosts, err := r.hosts(host)
   450  	if err != nil {
   451  		return nil, err
   452  	}
   453  	return &dockerBase{
   454  		refspec:    refspec,
   455  		repository: strings.TrimPrefix(refspec.Locator, host+"/"),
   456  		hosts:      hosts,
   457  		header:     r.header,
   458  	}, nil
   459  }
   460  
   461  func (r *dockerBase) filterHosts(caps HostCapabilities) (hosts []RegistryHost) {
   462  	for _, host := range r.hosts {
   463  		if host.Capabilities.Has(caps) {
   464  			hosts = append(hosts, host)
   465  		}
   466  	}
   467  	return
   468  }
   469  
   470  func (r *dockerBase) request(host RegistryHost, method string, ps ...string) *request {
   471  	header := http.Header{}
   472  	for key, value := range r.header {
   473  		header[key] = append(header[key], value...)
   474  	}
   475  	for key, value := range host.Header {
   476  		header[key] = append(header[key], value...)
   477  	}
   478  	parts := append([]string{"/", host.Path, r.repository}, ps...)
   479  	p := path.Join(parts...)
   480  	// Join strips trailing slash, re-add ending "/" if included
   481  	if len(parts) > 0 && strings.HasSuffix(parts[len(parts)-1], "/") {
   482  		p = p + "/"
   483  	}
   484  	return &request{
   485  		method: method,
   486  		path:   p,
   487  		header: header,
   488  		host:   host,
   489  	}
   490  }
   491  
   492  func (r *request) authorize(ctx context.Context, req *http.Request) error {
   493  	// Check if has header for host
   494  	if r.host.Authorizer != nil {
   495  		if err := r.host.Authorizer.Authorize(ctx, req); err != nil {
   496  			return err
   497  		}
   498  	}
   499  
   500  	return nil
   501  }
   502  
   503  func (r *request) addNamespace(ns string) (err error) {
   504  	if !r.host.isProxy(ns) {
   505  		return nil
   506  	}
   507  	var q url.Values
   508  	// Parse query
   509  	if i := strings.IndexByte(r.path, '?'); i > 0 {
   510  		r.path = r.path[:i+1]
   511  		q, err = url.ParseQuery(r.path[i+1:])
   512  		if err != nil {
   513  			return
   514  		}
   515  	} else {
   516  		r.path = r.path + "?"
   517  		q = url.Values{}
   518  	}
   519  	q.Add("ns", ns)
   520  
   521  	r.path = r.path + q.Encode()
   522  
   523  	return
   524  }
   525  
   526  type request struct {
   527  	method string
   528  	path   string
   529  	header http.Header
   530  	host   RegistryHost
   531  	body   func() (io.ReadCloser, error)
   532  	size   int64
   533  }
   534  
   535  func (r *request) do(ctx context.Context) (*http.Response, error) {
   536  	u := r.host.Scheme + "://" + r.host.Host + r.path
   537  	req, err := http.NewRequest(r.method, u, nil)
   538  	if err != nil {
   539  		return nil, err
   540  	}
   541  	req.Header = http.Header{} // headers need to be copied to avoid concurrent map access
   542  	for k, v := range r.header {
   543  		req.Header[k] = v
   544  	}
   545  	if r.body != nil {
   546  		body, err := r.body()
   547  		if err != nil {
   548  			return nil, err
   549  		}
   550  		req.Body = body
   551  		req.GetBody = r.body
   552  		if r.size > 0 {
   553  			req.ContentLength = r.size
   554  		}
   555  	}
   556  
   557  	ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", u))
   558  	log.G(ctx).WithFields(requestFields(req)).Debug("do request")
   559  	if err := r.authorize(ctx, req); err != nil {
   560  		return nil, errors.Wrap(err, "failed to authorize")
   561  	}
   562  
   563  	var client = &http.Client{}
   564  	if r.host.Client != nil {
   565  		*client = *r.host.Client
   566  	}
   567  	if client.CheckRedirect == nil {
   568  		client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
   569  			if len(via) >= 10 {
   570  				return errors.New("stopped after 10 redirects")
   571  			}
   572  			return errors.Wrap(r.authorize(ctx, req), "failed to authorize redirect")
   573  		}
   574  	}
   575  
   576  	resp, err := ctxhttp.Do(ctx, client, req)
   577  	if err != nil {
   578  		return nil, errors.Wrap(err, "failed to do request")
   579  	}
   580  	log.G(ctx).WithFields(responseFields(resp)).Debug("fetch response received")
   581  	return resp, nil
   582  }
   583  
   584  func (r *request) doWithRetries(ctx context.Context, responses []*http.Response) (*http.Response, error) {
   585  	resp, err := r.do(ctx)
   586  	if err != nil {
   587  		return nil, err
   588  	}
   589  
   590  	responses = append(responses, resp)
   591  	retry, err := r.retryRequest(ctx, responses)
   592  	if err != nil {
   593  		resp.Body.Close()
   594  		return nil, err
   595  	}
   596  	if retry {
   597  		resp.Body.Close()
   598  		return r.doWithRetries(ctx, responses)
   599  	}
   600  	return resp, err
   601  }
   602  
   603  func (r *request) retryRequest(ctx context.Context, responses []*http.Response) (bool, error) {
   604  	if len(responses) > 5 {
   605  		return false, nil
   606  	}
   607  	last := responses[len(responses)-1]
   608  	switch last.StatusCode {
   609  	case http.StatusUnauthorized:
   610  		log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
   611  		if r.host.Authorizer != nil {
   612  			if err := r.host.Authorizer.AddResponses(ctx, responses); err == nil {
   613  				return true, nil
   614  			} else if !errdefs.IsNotImplemented(err) {
   615  				return false, err
   616  			}
   617  		}
   618  
   619  		return false, nil
   620  	case http.StatusMethodNotAllowed:
   621  		// Support registries which have not properly implemented the HEAD method for
   622  		// manifests endpoint
   623  		if r.method == http.MethodHead && strings.Contains(r.path, "/manifests/") {
   624  			r.method = http.MethodGet
   625  			return true, nil
   626  		}
   627  	case http.StatusRequestTimeout, http.StatusTooManyRequests:
   628  		return true, nil
   629  	}
   630  
   631  	// TODO: Handle 50x errors accounting for attempt history
   632  	return false, nil
   633  }
   634  
   635  func (r *request) String() string {
   636  	return r.host.Scheme + "://" + r.host.Host + r.path
   637  }
   638  
   639  func requestFields(req *http.Request) logrus.Fields {
   640  	fields := map[string]interface{}{
   641  		"request.method": req.Method,
   642  	}
   643  	for k, vals := range req.Header {
   644  		k = strings.ToLower(k)
   645  		if k == "authorization" {
   646  			continue
   647  		}
   648  		for i, v := range vals {
   649  			field := "request.header." + k
   650  			if i > 0 {
   651  				field = fmt.Sprintf("%s.%d", field, i)
   652  			}
   653  			fields[field] = v
   654  		}
   655  	}
   656  
   657  	return logrus.Fields(fields)
   658  }
   659  
   660  func responseFields(resp *http.Response) logrus.Fields {
   661  	fields := map[string]interface{}{
   662  		"response.status": resp.Status,
   663  	}
   664  	for k, vals := range resp.Header {
   665  		k = strings.ToLower(k)
   666  		for i, v := range vals {
   667  			field := "response.header." + k
   668  			if i > 0 {
   669  				field = fmt.Sprintf("%s.%d", field, i)
   670  			}
   671  			fields[field] = v
   672  		}
   673  	}
   674  
   675  	return logrus.Fields(fields)
   676  }