...

Source file src/k8s.io/cli-runtime/pkg/resource/builder.go

Documentation: k8s.io/cli-runtime/pkg/resource

     1  /*
     2  Copyright 2014 The Kubernetes 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 resource
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"net/url"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"sync"
    28  
    29  	"k8s.io/apimachinery/pkg/api/meta"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
    33  	"k8s.io/apimachinery/pkg/labels"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	"k8s.io/apimachinery/pkg/runtime/serializer"
    37  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    38  	"k8s.io/apimachinery/pkg/util/sets"
    39  	"k8s.io/client-go/discovery"
    40  	"k8s.io/client-go/rest"
    41  	"k8s.io/client-go/restmapper"
    42  	"sigs.k8s.io/kustomize/kyaml/filesys"
    43  )
    44  
    45  var FileExtensions = []string{".json", ".yaml", ".yml"}
    46  var InputExtensions = append(FileExtensions, "stdin")
    47  
    48  const defaultHttpGetAttempts = 3
    49  const pathNotExistError = "the path %q does not exist"
    50  
    51  // Builder provides convenience functions for taking arguments and parameters
    52  // from the command line and converting them to a list of resources to iterate
    53  // over using the Visitor interface.
    54  type Builder struct {
    55  	categoryExpanderFn CategoryExpanderFunc
    56  
    57  	// mapper is set explicitly by resource builders
    58  	mapper *mapper
    59  
    60  	// clientConfigFn is a function to produce a client, *if* you need one
    61  	clientConfigFn ClientConfigFunc
    62  
    63  	restMapperFn RESTMapperFunc
    64  
    65  	// objectTyper is statically determinant per-command invocation based on your internal or unstructured choice
    66  	// it does not ever need to rely upon discovery.
    67  	objectTyper runtime.ObjectTyper
    68  
    69  	// codecFactory describes which codecs you want to use
    70  	negotiatedSerializer runtime.NegotiatedSerializer
    71  
    72  	// local indicates that we cannot make server calls
    73  	local bool
    74  
    75  	errs []error
    76  
    77  	paths      []Visitor
    78  	stream     bool
    79  	stdinInUse bool
    80  	dir        bool
    81  
    82  	visitorConcurrency int
    83  
    84  	labelSelector     *string
    85  	fieldSelector     *string
    86  	selectAll         bool
    87  	limitChunks       int64
    88  	requestTransforms []RequestTransform
    89  
    90  	resources   []string
    91  	subresource string
    92  
    93  	namespace    string
    94  	allNamespace bool
    95  	names        []string
    96  
    97  	resourceTuples []resourceTuple
    98  
    99  	defaultNamespace bool
   100  	requireNamespace bool
   101  
   102  	flatten bool
   103  	latest  bool
   104  
   105  	requireObject bool
   106  
   107  	singleResourceType bool
   108  	continueOnError    bool
   109  
   110  	singleItemImplied bool
   111  
   112  	schema ContentValidator
   113  
   114  	// fakeClientFn is used for testing
   115  	fakeClientFn FakeClientFunc
   116  }
   117  
   118  var missingResourceError = fmt.Errorf(`You must provide one or more resources by argument or filename.
   119  Example resource specifications include:
   120     '-f rsrc.yaml'
   121     '--filename=rsrc.json'
   122     '<resource> <name>'
   123     '<resource>'`)
   124  
   125  var LocalResourceError = errors.New(`error: you must specify resources by --filename when --local is set.
   126  Example resource specifications include:
   127     '-f rsrc.yaml'
   128     '--filename=rsrc.json'`)
   129  
   130  var StdinMultiUseError = errors.New("standard input cannot be used for multiple arguments")
   131  
   132  // TODO: expand this to include other errors.
   133  func IsUsageError(err error) bool {
   134  	if err == nil {
   135  		return false
   136  	}
   137  	return err == missingResourceError
   138  }
   139  
   140  type FilenameOptions struct {
   141  	Filenames []string
   142  	Kustomize string
   143  	Recursive bool
   144  }
   145  
   146  func (o *FilenameOptions) validate() []error {
   147  	var errs []error
   148  	if len(o.Filenames) > 0 && len(o.Kustomize) > 0 {
   149  		errs = append(errs, fmt.Errorf("only one of -f or -k can be specified"))
   150  	}
   151  	if len(o.Kustomize) > 0 && o.Recursive {
   152  		errs = append(errs, fmt.Errorf("the -k flag can't be used with -f or -R"))
   153  	}
   154  	return errs
   155  }
   156  
   157  func (o *FilenameOptions) RequireFilenameOrKustomize() error {
   158  	if len(o.Filenames) == 0 && len(o.Kustomize) == 0 {
   159  		return fmt.Errorf("must specify one of -f and -k")
   160  	}
   161  	return nil
   162  }
   163  
   164  type resourceTuple struct {
   165  	Resource string
   166  	Name     string
   167  }
   168  
   169  type FakeClientFunc func(version schema.GroupVersion) (RESTClient, error)
   170  
   171  func NewFakeBuilder(fakeClientFn FakeClientFunc, restMapper RESTMapperFunc, categoryExpander CategoryExpanderFunc) *Builder {
   172  	ret := newBuilder(nil, restMapper, categoryExpander)
   173  	ret.fakeClientFn = fakeClientFn
   174  	return ret
   175  }
   176  
   177  // NewBuilder creates a builder that operates on generic objects. At least one of
   178  // internal or unstructured must be specified.
   179  // TODO: Add versioned client (although versioned is still lossy)
   180  // TODO remove internal and unstructured mapper and instead have them set the negotiated serializer for use in the client
   181  func newBuilder(clientConfigFn ClientConfigFunc, restMapper RESTMapperFunc, categoryExpander CategoryExpanderFunc) *Builder {
   182  	return &Builder{
   183  		clientConfigFn:     clientConfigFn,
   184  		restMapperFn:       restMapper,
   185  		categoryExpanderFn: categoryExpander,
   186  		requireObject:      true,
   187  	}
   188  }
   189  
   190  // noopClientGetter implements RESTClientGetter returning only errors.
   191  // used as a dummy getter in a local-only builder.
   192  type noopClientGetter struct{}
   193  
   194  func (noopClientGetter) ToRESTConfig() (*rest.Config, error) {
   195  	return nil, fmt.Errorf("local operation only")
   196  }
   197  func (noopClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
   198  	return nil, fmt.Errorf("local operation only")
   199  }
   200  func (noopClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
   201  	return nil, fmt.Errorf("local operation only")
   202  }
   203  
   204  // NewLocalBuilder returns a builder that is configured not to create REST clients and avoids asking the server for results.
   205  func NewLocalBuilder() *Builder {
   206  	return NewBuilder(noopClientGetter{}).Local()
   207  }
   208  
   209  func NewBuilder(restClientGetter RESTClientGetter) *Builder {
   210  	categoryExpanderFn := func() (restmapper.CategoryExpander, error) {
   211  		discoveryClient, err := restClientGetter.ToDiscoveryClient()
   212  		if err != nil {
   213  			return nil, err
   214  		}
   215  		return restmapper.NewDiscoveryCategoryExpander(discoveryClient), err
   216  	}
   217  
   218  	return newBuilder(
   219  		restClientGetter.ToRESTConfig,
   220  		restClientGetter.ToRESTMapper,
   221  		(&cachingCategoryExpanderFunc{delegate: categoryExpanderFn}).ToCategoryExpander,
   222  	)
   223  }
   224  
   225  func (b *Builder) Schema(schema ContentValidator) *Builder {
   226  	b.schema = schema
   227  	return b
   228  }
   229  
   230  func (b *Builder) AddError(err error) *Builder {
   231  	if err == nil {
   232  		return b
   233  	}
   234  	b.errs = append(b.errs, err)
   235  	return b
   236  }
   237  
   238  // VisitorConcurrency sets the number of concurrent visitors to use when
   239  // visiting lists.
   240  func (b *Builder) VisitorConcurrency(concurrency int) *Builder {
   241  	b.visitorConcurrency = concurrency
   242  	return b
   243  }
   244  
   245  // FilenameParam groups input in two categories: URLs and files (files, directories, STDIN)
   246  // If enforceNamespace is false, namespaces in the specs will be allowed to
   247  // override the default namespace. If it is true, namespaces that don't match
   248  // will cause an error.
   249  // If ContinueOnError() is set prior to this method, objects on the path that are not
   250  // recognized will be ignored (but logged at V(2)).
   251  func (b *Builder) FilenameParam(enforceNamespace bool, filenameOptions *FilenameOptions) *Builder {
   252  	if errs := filenameOptions.validate(); len(errs) > 0 {
   253  		b.errs = append(b.errs, errs...)
   254  		return b
   255  	}
   256  	recursive := filenameOptions.Recursive
   257  	paths := filenameOptions.Filenames
   258  	for _, s := range paths {
   259  		switch {
   260  		case s == "-":
   261  			b.Stdin()
   262  		case strings.Index(s, "http://") == 0 || strings.Index(s, "https://") == 0:
   263  			url, err := url.Parse(s)
   264  			if err != nil {
   265  				b.errs = append(b.errs, fmt.Errorf("the URL passed to filename %q is not valid: %v", s, err))
   266  				continue
   267  			}
   268  			b.URL(defaultHttpGetAttempts, url)
   269  		default:
   270  			matches, err := expandIfFilePattern(s)
   271  			if err != nil {
   272  				b.errs = append(b.errs, err)
   273  				continue
   274  			}
   275  			if !recursive && len(matches) == 1 {
   276  				b.singleItemImplied = true
   277  			}
   278  			b.Path(recursive, matches...)
   279  		}
   280  	}
   281  	if filenameOptions.Kustomize != "" {
   282  		b.paths = append(
   283  			b.paths,
   284  			&KustomizeVisitor{
   285  				mapper:  b.mapper,
   286  				dirPath: filenameOptions.Kustomize,
   287  				schema:  b.schema,
   288  				fSys:    filesys.MakeFsOnDisk(),
   289  			})
   290  	}
   291  
   292  	if enforceNamespace {
   293  		b.RequireNamespace()
   294  	}
   295  
   296  	return b
   297  }
   298  
   299  // Unstructured updates the builder so that it will request and send unstructured
   300  // objects. Unstructured objects preserve all fields sent by the server in a map format
   301  // based on the object's JSON structure which means no data is lost when the client
   302  // reads and then writes an object. Use this mode in preference to Internal unless you
   303  // are working with Go types directly.
   304  func (b *Builder) Unstructured() *Builder {
   305  	if b.mapper != nil {
   306  		b.errs = append(b.errs, fmt.Errorf("another mapper was already selected, cannot use unstructured types"))
   307  		return b
   308  	}
   309  	b.objectTyper = unstructuredscheme.NewUnstructuredObjectTyper()
   310  	b.mapper = &mapper{
   311  		localFn:      b.isLocal,
   312  		restMapperFn: b.restMapperFn,
   313  		clientFn:     b.getClient,
   314  		decoder:      &metadataValidatingDecoder{unstructured.UnstructuredJSONScheme},
   315  	}
   316  
   317  	return b
   318  }
   319  
   320  // WithScheme uses the scheme to manage typing, conversion (optional), and decoding.  If decodingVersions
   321  // is empty, then you can end up with internal types.  You have been warned.
   322  func (b *Builder) WithScheme(scheme *runtime.Scheme, decodingVersions ...schema.GroupVersion) *Builder {
   323  	if b.mapper != nil {
   324  		b.errs = append(b.errs, fmt.Errorf("another mapper was already selected, cannot use internal types"))
   325  		return b
   326  	}
   327  	b.objectTyper = scheme
   328  	codecFactory := serializer.NewCodecFactory(scheme)
   329  	negotiatedSerializer := runtime.NegotiatedSerializer(codecFactory)
   330  	// if you specified versions, you're specifying a desire for external types, which you don't want to round-trip through
   331  	// internal types
   332  	if len(decodingVersions) > 0 {
   333  		negotiatedSerializer = codecFactory.WithoutConversion()
   334  	}
   335  	b.negotiatedSerializer = negotiatedSerializer
   336  
   337  	b.mapper = &mapper{
   338  		localFn:      b.isLocal,
   339  		restMapperFn: b.restMapperFn,
   340  		clientFn:     b.getClient,
   341  		decoder:      codecFactory.UniversalDecoder(decodingVersions...),
   342  	}
   343  
   344  	return b
   345  }
   346  
   347  // LocalParam calls Local() if local is true.
   348  func (b *Builder) LocalParam(local bool) *Builder {
   349  	if local {
   350  		b.Local()
   351  	}
   352  	return b
   353  }
   354  
   355  // Local will avoid asking the server for results.
   356  func (b *Builder) Local() *Builder {
   357  	b.local = true
   358  	return b
   359  }
   360  
   361  func (b *Builder) isLocal() bool {
   362  	return b.local
   363  }
   364  
   365  // Mapper returns a copy of the current mapper.
   366  func (b *Builder) Mapper() *mapper {
   367  	mapper := *b.mapper
   368  	return &mapper
   369  }
   370  
   371  // URL accepts a number of URLs directly.
   372  func (b *Builder) URL(httpAttemptCount int, urls ...*url.URL) *Builder {
   373  	for _, u := range urls {
   374  		b.paths = append(b.paths, &URLVisitor{
   375  			URL:              u,
   376  			StreamVisitor:    NewStreamVisitor(nil, b.mapper, u.String(), b.schema),
   377  			HttpAttemptCount: httpAttemptCount,
   378  		})
   379  	}
   380  	return b
   381  }
   382  
   383  // Stdin will read objects from the standard input. If ContinueOnError() is set
   384  // prior to this method being called, objects in the stream that are unrecognized
   385  // will be ignored (but logged at V(2)). If StdinInUse() is set prior to this method
   386  // being called, an error will be recorded as there are multiple entities trying to use
   387  // the single standard input stream.
   388  func (b *Builder) Stdin() *Builder {
   389  	b.stream = true
   390  	if b.stdinInUse {
   391  		b.errs = append(b.errs, StdinMultiUseError)
   392  	}
   393  	b.stdinInUse = true
   394  	b.paths = append(b.paths, FileVisitorForSTDIN(b.mapper, b.schema))
   395  	return b
   396  }
   397  
   398  // StdinInUse will mark standard input as in use by this Builder, and therefore standard
   399  // input should not be used by another entity. If Stdin() is set prior to this method
   400  // being called, an error will be recorded as there are multiple entities trying to use
   401  // the single standard input stream.
   402  func (b *Builder) StdinInUse() *Builder {
   403  	if b.stdinInUse {
   404  		b.errs = append(b.errs, StdinMultiUseError)
   405  	}
   406  	b.stdinInUse = true
   407  	return b
   408  }
   409  
   410  // Stream will read objects from the provided reader, and if an error occurs will
   411  // include the name string in the error message. If ContinueOnError() is set
   412  // prior to this method being called, objects in the stream that are unrecognized
   413  // will be ignored (but logged at V(2)).
   414  func (b *Builder) Stream(r io.Reader, name string) *Builder {
   415  	b.stream = true
   416  	b.paths = append(b.paths, NewStreamVisitor(r, b.mapper, name, b.schema))
   417  	return b
   418  }
   419  
   420  // Path accepts a set of paths that may be files, directories (all can containing
   421  // one or more resources). Creates a FileVisitor for each file and then each
   422  // FileVisitor is streaming the content to a StreamVisitor. If ContinueOnError() is set
   423  // prior to this method being called, objects on the path that are unrecognized will be
   424  // ignored (but logged at V(2)).
   425  func (b *Builder) Path(recursive bool, paths ...string) *Builder {
   426  	for _, p := range paths {
   427  		_, err := os.Stat(p)
   428  		if os.IsNotExist(err) {
   429  			b.errs = append(b.errs, fmt.Errorf(pathNotExistError, p))
   430  			continue
   431  		}
   432  		if err != nil {
   433  			b.errs = append(b.errs, fmt.Errorf("the path %q cannot be accessed: %v", p, err))
   434  			continue
   435  		}
   436  
   437  		visitors, err := ExpandPathsToFileVisitors(b.mapper, p, recursive, FileExtensions, b.schema)
   438  		if err != nil {
   439  			b.errs = append(b.errs, fmt.Errorf("error reading %q: %v", p, err))
   440  		}
   441  		if len(visitors) > 1 {
   442  			b.dir = true
   443  		}
   444  
   445  		b.paths = append(b.paths, visitors...)
   446  	}
   447  	if len(b.paths) == 0 && len(b.errs) == 0 {
   448  		b.errs = append(b.errs, fmt.Errorf("error reading %v: recognized file extensions are %v", paths, FileExtensions))
   449  	}
   450  	return b
   451  }
   452  
   453  // ResourceTypes is a list of types of resources to operate on, when listing objects on
   454  // the server or retrieving objects that match a selector.
   455  func (b *Builder) ResourceTypes(types ...string) *Builder {
   456  	b.resources = append(b.resources, types...)
   457  	return b
   458  }
   459  
   460  // ResourceNames accepts a default type and one or more names, and creates tuples of
   461  // resources
   462  func (b *Builder) ResourceNames(resource string, names ...string) *Builder {
   463  	for _, name := range names {
   464  		// See if this input string is of type/name format
   465  		tuple, ok, err := splitResourceTypeName(name)
   466  		if err != nil {
   467  			b.errs = append(b.errs, err)
   468  			return b
   469  		}
   470  
   471  		if ok {
   472  			b.resourceTuples = append(b.resourceTuples, tuple)
   473  			continue
   474  		}
   475  		if len(resource) == 0 {
   476  			b.errs = append(b.errs, fmt.Errorf("the argument %q must be RESOURCE/NAME", name))
   477  			continue
   478  		}
   479  
   480  		// Use the given default type to create a resource tuple
   481  		b.resourceTuples = append(b.resourceTuples, resourceTuple{Resource: resource, Name: name})
   482  	}
   483  	return b
   484  }
   485  
   486  // LabelSelectorParam defines a selector that should be applied to the object types to load.
   487  // This will not affect files loaded from disk or URL. If the parameter is empty it is
   488  // a no-op - to select all resources invoke `b.LabelSelector(labels.Everything.String)`.
   489  func (b *Builder) LabelSelectorParam(s string) *Builder {
   490  	selector := strings.TrimSpace(s)
   491  	if len(selector) == 0 {
   492  		return b
   493  	}
   494  	if b.selectAll {
   495  		b.errs = append(b.errs, fmt.Errorf("found non-empty label selector %q with previously set 'all' parameter. ", s))
   496  		return b
   497  	}
   498  	return b.LabelSelector(selector)
   499  }
   500  
   501  // LabelSelector accepts a selector directly and will filter the resulting list by that object.
   502  // Use LabelSelectorParam instead for user input.
   503  func (b *Builder) LabelSelector(selector string) *Builder {
   504  	if len(selector) == 0 {
   505  		return b
   506  	}
   507  
   508  	b.labelSelector = &selector
   509  	return b
   510  }
   511  
   512  // FieldSelectorParam defines a selector that should be applied to the object types to load.
   513  // This will not affect files loaded from disk or URL. If the parameter is empty it is
   514  // a no-op - to select all resources.
   515  func (b *Builder) FieldSelectorParam(s string) *Builder {
   516  	s = strings.TrimSpace(s)
   517  	if len(s) == 0 {
   518  		return b
   519  	}
   520  	if b.selectAll {
   521  		b.errs = append(b.errs, fmt.Errorf("found non-empty field selector %q with previously set 'all' parameter. ", s))
   522  		return b
   523  	}
   524  	b.fieldSelector = &s
   525  	return b
   526  }
   527  
   528  // NamespaceParam accepts the namespace that these resources should be
   529  // considered under from - used by DefaultNamespace() and RequireNamespace()
   530  func (b *Builder) NamespaceParam(namespace string) *Builder {
   531  	b.namespace = namespace
   532  	return b
   533  }
   534  
   535  // DefaultNamespace instructs the builder to set the namespace value for any object found
   536  // to NamespaceParam() if empty.
   537  func (b *Builder) DefaultNamespace() *Builder {
   538  	b.defaultNamespace = true
   539  	return b
   540  }
   541  
   542  // AllNamespaces instructs the builder to metav1.NamespaceAll as a namespace to request resources
   543  // across all of the namespace. This overrides the namespace set by NamespaceParam().
   544  func (b *Builder) AllNamespaces(allNamespace bool) *Builder {
   545  	if allNamespace {
   546  		b.namespace = metav1.NamespaceAll
   547  	}
   548  	b.allNamespace = allNamespace
   549  	return b
   550  }
   551  
   552  // RequireNamespace instructs the builder to set the namespace value for any object found
   553  // to NamespaceParam() if empty, and if the value on the resource does not match
   554  // NamespaceParam() an error will be returned.
   555  func (b *Builder) RequireNamespace() *Builder {
   556  	b.requireNamespace = true
   557  	return b
   558  }
   559  
   560  // RequestChunksOf attempts to load responses from the server in batches of size limit
   561  // to avoid long delays loading and transferring very large lists. If unset defaults to
   562  // no chunking.
   563  func (b *Builder) RequestChunksOf(chunkSize int64) *Builder {
   564  	b.limitChunks = chunkSize
   565  	return b
   566  }
   567  
   568  // TransformRequests alters API calls made by clients requested from this builder. Pass
   569  // an empty list to clear modifiers.
   570  func (b *Builder) TransformRequests(opts ...RequestTransform) *Builder {
   571  	b.requestTransforms = opts
   572  	return b
   573  }
   574  
   575  // Subresource instructs the builder to retrieve the object at the
   576  // subresource path instead of the main resource path.
   577  func (b *Builder) Subresource(subresource string) *Builder {
   578  	b.subresource = subresource
   579  	return b
   580  }
   581  
   582  // SelectEverythingParam
   583  func (b *Builder) SelectAllParam(selectAll bool) *Builder {
   584  	if selectAll && (b.labelSelector != nil || b.fieldSelector != nil) {
   585  		b.errs = append(b.errs, fmt.Errorf("setting 'all' parameter but found a non empty selector. "))
   586  		return b
   587  	}
   588  	b.selectAll = selectAll
   589  	return b
   590  }
   591  
   592  // ResourceTypeOrNameArgs indicates that the builder should accept arguments
   593  // of the form `(<type1>[,<type2>,...]|<type> <name1>[,<name2>,...])`. When one argument is
   594  // received, the types provided will be retrieved from the server (and be comma delimited).
   595  // When two or more arguments are received, they must be a single type and resource name(s).
   596  // The allowEmptySelector permits to select all the resources (via Everything func).
   597  func (b *Builder) ResourceTypeOrNameArgs(allowEmptySelector bool, args ...string) *Builder {
   598  	args = normalizeMultipleResourcesArgs(args)
   599  	if ok, err := hasCombinedTypeArgs(args); ok {
   600  		if err != nil {
   601  			b.errs = append(b.errs, err)
   602  			return b
   603  		}
   604  		for _, s := range args {
   605  			tuple, ok, err := splitResourceTypeName(s)
   606  			if err != nil {
   607  				b.errs = append(b.errs, err)
   608  				return b
   609  			}
   610  			if ok {
   611  				b.resourceTuples = append(b.resourceTuples, tuple)
   612  			}
   613  		}
   614  		return b
   615  	}
   616  	if len(args) > 0 {
   617  		// Try replacing aliases only in types
   618  		args[0] = b.ReplaceAliases(args[0])
   619  	}
   620  	switch {
   621  	case len(args) > 2:
   622  		b.names = append(b.names, args[1:]...)
   623  		b.ResourceTypes(SplitResourceArgument(args[0])...)
   624  	case len(args) == 2:
   625  		b.names = append(b.names, args[1])
   626  		b.ResourceTypes(SplitResourceArgument(args[0])...)
   627  	case len(args) == 1:
   628  		b.ResourceTypes(SplitResourceArgument(args[0])...)
   629  		if b.labelSelector == nil && allowEmptySelector {
   630  			selector := labels.Everything().String()
   631  			b.labelSelector = &selector
   632  		}
   633  	case len(args) == 0:
   634  	default:
   635  		b.errs = append(b.errs, fmt.Errorf("arguments must consist of a resource or a resource and name"))
   636  	}
   637  	return b
   638  }
   639  
   640  // ReplaceAliases accepts an argument and tries to expand any existing
   641  // aliases found in it
   642  func (b *Builder) ReplaceAliases(input string) string {
   643  	replaced := []string{}
   644  	for _, arg := range strings.Split(input, ",") {
   645  		if b.categoryExpanderFn == nil {
   646  			continue
   647  		}
   648  		categoryExpander, err := b.categoryExpanderFn()
   649  		if err != nil {
   650  			b.AddError(err)
   651  			continue
   652  		}
   653  
   654  		if resources, ok := categoryExpander.Expand(arg); ok {
   655  			asStrings := []string{}
   656  			for _, resource := range resources {
   657  				if len(resource.Group) == 0 {
   658  					asStrings = append(asStrings, resource.Resource)
   659  					continue
   660  				}
   661  				asStrings = append(asStrings, resource.Resource+"."+resource.Group)
   662  			}
   663  			arg = strings.Join(asStrings, ",")
   664  		}
   665  		replaced = append(replaced, arg)
   666  	}
   667  	return strings.Join(replaced, ",")
   668  }
   669  
   670  func hasCombinedTypeArgs(args []string) (bool, error) {
   671  	hasSlash := 0
   672  	for _, s := range args {
   673  		if strings.Contains(s, "/") {
   674  			hasSlash++
   675  		}
   676  	}
   677  	switch {
   678  	case hasSlash > 0 && hasSlash == len(args):
   679  		return true, nil
   680  	case hasSlash > 0 && hasSlash != len(args):
   681  		baseCmd := "cmd"
   682  		if len(os.Args) > 0 {
   683  			baseCmdSlice := strings.Split(os.Args[0], "/")
   684  			baseCmd = baseCmdSlice[len(baseCmdSlice)-1]
   685  		}
   686  		return true, fmt.Errorf("there is no need to specify a resource type as a separate argument when passing arguments in resource/name form (e.g. '%s get resource/<resource_name>' instead of '%s get resource resource/<resource_name>'", baseCmd, baseCmd)
   687  	default:
   688  		return false, nil
   689  	}
   690  }
   691  
   692  // Normalize args convert multiple resources to resource tuples, a,b,c d
   693  // as a transform to a/d b/d c/d
   694  func normalizeMultipleResourcesArgs(args []string) []string {
   695  	if len(args) >= 2 {
   696  		resources := []string{}
   697  		resources = append(resources, SplitResourceArgument(args[0])...)
   698  		if len(resources) > 1 {
   699  			names := []string{}
   700  			names = append(names, args[1:]...)
   701  			newArgs := []string{}
   702  			for _, resource := range resources {
   703  				for _, name := range names {
   704  					newArgs = append(newArgs, strings.Join([]string{resource, name}, "/"))
   705  				}
   706  			}
   707  			return newArgs
   708  		}
   709  	}
   710  	return args
   711  }
   712  
   713  // splitResourceTypeName handles type/name resource formats and returns a resource tuple
   714  // (empty or not), whether it successfully found one, and an error
   715  func splitResourceTypeName(s string) (resourceTuple, bool, error) {
   716  	if !strings.Contains(s, "/") {
   717  		return resourceTuple{}, false, nil
   718  	}
   719  	seg := strings.Split(s, "/")
   720  	if len(seg) != 2 {
   721  		return resourceTuple{}, false, fmt.Errorf("arguments in resource/name form may not have more than one slash")
   722  	}
   723  	resource, name := seg[0], seg[1]
   724  	if len(resource) == 0 || len(name) == 0 || len(SplitResourceArgument(resource)) != 1 {
   725  		return resourceTuple{}, false, fmt.Errorf("arguments in resource/name form must have a single resource and name")
   726  	}
   727  	return resourceTuple{Resource: resource, Name: name}, true, nil
   728  }
   729  
   730  // Flatten will convert any objects with a field named "Items" that is an array of runtime.Object
   731  // compatible types into individual entries and give them their own items. The original object
   732  // is not passed to any visitors.
   733  func (b *Builder) Flatten() *Builder {
   734  	b.flatten = true
   735  	return b
   736  }
   737  
   738  // Latest will fetch the latest copy of any objects loaded from URLs or files from the server.
   739  func (b *Builder) Latest() *Builder {
   740  	b.latest = true
   741  	return b
   742  }
   743  
   744  // RequireObject ensures that resulting infos have an object set. If false, resulting info may not have an object set.
   745  func (b *Builder) RequireObject(require bool) *Builder {
   746  	b.requireObject = require
   747  	return b
   748  }
   749  
   750  // ContinueOnError will attempt to load and visit as many objects as possible, even if some visits
   751  // return errors or some objects cannot be loaded. The default behavior is to terminate after
   752  // the first error is returned from a VisitorFunc.
   753  func (b *Builder) ContinueOnError() *Builder {
   754  	b.continueOnError = true
   755  	return b
   756  }
   757  
   758  // SingleResourceType will cause the builder to error if the user specifies more than a single type
   759  // of resource.
   760  func (b *Builder) SingleResourceType() *Builder {
   761  	b.singleResourceType = true
   762  	return b
   763  }
   764  
   765  // mappingFor returns the RESTMapping for the Kind given, or the Kind referenced by the resource.
   766  // Prefers a fully specified GroupVersionResource match. If one is not found, we match on a fully
   767  // specified GroupVersionKind, or fallback to a match on GroupKind.
   768  func (b *Builder) mappingFor(resourceOrKindArg string) (*meta.RESTMapping, error) {
   769  	fullySpecifiedGVR, groupResource := schema.ParseResourceArg(resourceOrKindArg)
   770  	gvk := schema.GroupVersionKind{}
   771  	restMapper, err := b.restMapperFn()
   772  	if err != nil {
   773  		return nil, err
   774  	}
   775  
   776  	if fullySpecifiedGVR != nil {
   777  		gvk, _ = restMapper.KindFor(*fullySpecifiedGVR)
   778  	}
   779  	if gvk.Empty() {
   780  		gvk, _ = restMapper.KindFor(groupResource.WithVersion(""))
   781  	}
   782  	if !gvk.Empty() {
   783  		return restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
   784  	}
   785  
   786  	fullySpecifiedGVK, groupKind := schema.ParseKindArg(resourceOrKindArg)
   787  	if fullySpecifiedGVK == nil {
   788  		gvk := groupKind.WithVersion("")
   789  		fullySpecifiedGVK = &gvk
   790  	}
   791  
   792  	if !fullySpecifiedGVK.Empty() {
   793  		if mapping, err := restMapper.RESTMapping(fullySpecifiedGVK.GroupKind(), fullySpecifiedGVK.Version); err == nil {
   794  			return mapping, nil
   795  		}
   796  	}
   797  
   798  	mapping, err := restMapper.RESTMapping(groupKind, gvk.Version)
   799  	if err != nil {
   800  		// if we error out here, it is because we could not match a resource or a kind
   801  		// for the given argument. To maintain consistency with previous behavior,
   802  		// announce that a resource type could not be found.
   803  		// if the error is _not_ a *meta.NoKindMatchError, then we had trouble doing discovery,
   804  		// so we should return the original error since it may help a user diagnose what is actually wrong
   805  		if meta.IsNoMatchError(err) {
   806  			return nil, fmt.Errorf("the server doesn't have a resource type %q", groupResource.Resource)
   807  		}
   808  		return nil, err
   809  	}
   810  
   811  	return mapping, nil
   812  }
   813  
   814  func (b *Builder) resourceMappings() ([]*meta.RESTMapping, error) {
   815  	if len(b.resources) > 1 && b.singleResourceType {
   816  		return nil, fmt.Errorf("you may only specify a single resource type")
   817  	}
   818  	mappings := []*meta.RESTMapping{}
   819  	seen := map[schema.GroupVersionKind]bool{}
   820  	for _, r := range b.resources {
   821  		mapping, err := b.mappingFor(r)
   822  		if err != nil {
   823  			return nil, err
   824  		}
   825  		// This ensures the mappings for resources(shortcuts, plural) unique
   826  		if seen[mapping.GroupVersionKind] {
   827  			continue
   828  		}
   829  		seen[mapping.GroupVersionKind] = true
   830  
   831  		mappings = append(mappings, mapping)
   832  	}
   833  	return mappings, nil
   834  }
   835  
   836  func (b *Builder) resourceTupleMappings() (map[string]*meta.RESTMapping, error) {
   837  	mappings := make(map[string]*meta.RESTMapping)
   838  	canonical := make(map[schema.GroupVersionResource]struct{})
   839  	for _, r := range b.resourceTuples {
   840  		if _, ok := mappings[r.Resource]; ok {
   841  			continue
   842  		}
   843  		mapping, err := b.mappingFor(r.Resource)
   844  		if err != nil {
   845  			return nil, err
   846  		}
   847  
   848  		mappings[r.Resource] = mapping
   849  		canonical[mapping.Resource] = struct{}{}
   850  	}
   851  	if len(canonical) > 1 && b.singleResourceType {
   852  		return nil, fmt.Errorf("you may only specify a single resource type")
   853  	}
   854  	return mappings, nil
   855  }
   856  
   857  func (b *Builder) visitorResult() *Result {
   858  	if len(b.errs) > 0 {
   859  		return &Result{err: utilerrors.NewAggregate(b.errs)}
   860  	}
   861  
   862  	if b.selectAll {
   863  		selector := labels.Everything().String()
   864  		b.labelSelector = &selector
   865  	}
   866  
   867  	// visit items specified by paths
   868  	if len(b.paths) != 0 {
   869  		return b.visitByPaths()
   870  	}
   871  
   872  	// visit selectors
   873  	if b.labelSelector != nil || b.fieldSelector != nil {
   874  		return b.visitBySelector()
   875  	}
   876  
   877  	// visit items specified by resource and name
   878  	if len(b.resourceTuples) != 0 {
   879  		return b.visitByResource()
   880  	}
   881  
   882  	// visit items specified by name
   883  	if len(b.names) != 0 {
   884  		return b.visitByName()
   885  	}
   886  
   887  	if len(b.resources) != 0 {
   888  		for _, r := range b.resources {
   889  			_, err := b.mappingFor(r)
   890  			if err != nil {
   891  				return &Result{err: err}
   892  			}
   893  		}
   894  		return &Result{err: fmt.Errorf("resource(s) were provided, but no name was specified")}
   895  	}
   896  	return &Result{err: missingResourceError}
   897  }
   898  
   899  func (b *Builder) visitBySelector() *Result {
   900  	result := &Result{
   901  		targetsSingleItems: false,
   902  	}
   903  
   904  	if len(b.names) != 0 {
   905  		return result.withError(fmt.Errorf("name cannot be provided when a selector is specified"))
   906  	}
   907  	if len(b.resourceTuples) != 0 {
   908  		return result.withError(fmt.Errorf("selectors and the all flag cannot be used when passing resource/name arguments"))
   909  	}
   910  	if len(b.resources) == 0 {
   911  		return result.withError(fmt.Errorf("at least one resource must be specified to use a selector"))
   912  	}
   913  	if len(b.subresource) != 0 {
   914  		return result.withError(fmt.Errorf("subresource cannot be used when bulk resources are specified"))
   915  	}
   916  
   917  	mappings, err := b.resourceMappings()
   918  	if err != nil {
   919  		result.err = err
   920  		return result
   921  	}
   922  
   923  	var labelSelector, fieldSelector string
   924  	if b.labelSelector != nil {
   925  		labelSelector = *b.labelSelector
   926  	}
   927  	if b.fieldSelector != nil {
   928  		fieldSelector = *b.fieldSelector
   929  	}
   930  
   931  	visitors := []Visitor{}
   932  	for _, mapping := range mappings {
   933  		client, err := b.getClient(mapping.GroupVersionKind.GroupVersion())
   934  		if err != nil {
   935  			result.err = err
   936  			return result
   937  		}
   938  		selectorNamespace := b.namespace
   939  		if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
   940  			selectorNamespace = ""
   941  		}
   942  		visitors = append(visitors, NewSelector(client, mapping, selectorNamespace, labelSelector, fieldSelector, b.limitChunks))
   943  	}
   944  	if b.continueOnError {
   945  		result.visitor = EagerVisitorList(visitors)
   946  	} else {
   947  		result.visitor = VisitorList(visitors)
   948  	}
   949  	result.sources = visitors
   950  	return result
   951  }
   952  
   953  func (b *Builder) getClient(gv schema.GroupVersion) (RESTClient, error) {
   954  	var (
   955  		client RESTClient
   956  		err    error
   957  	)
   958  
   959  	switch {
   960  	case b.fakeClientFn != nil:
   961  		client, err = b.fakeClientFn(gv)
   962  	case b.negotiatedSerializer != nil:
   963  		client, err = b.clientConfigFn.withStdinUnavailable(b.stdinInUse).clientForGroupVersion(gv, b.negotiatedSerializer)
   964  	default:
   965  		client, err = b.clientConfigFn.withStdinUnavailable(b.stdinInUse).unstructuredClientForGroupVersion(gv)
   966  	}
   967  
   968  	if err != nil {
   969  		return nil, err
   970  	}
   971  
   972  	return NewClientWithOptions(client, b.requestTransforms...), nil
   973  }
   974  
   975  func (b *Builder) visitByResource() *Result {
   976  	// if b.singleItemImplied is false, this could be by default, so double-check length
   977  	// of resourceTuples to determine if in fact it is singleItemImplied or not
   978  	isSingleItemImplied := b.singleItemImplied
   979  	if !isSingleItemImplied {
   980  		isSingleItemImplied = len(b.resourceTuples) == 1
   981  	}
   982  
   983  	result := &Result{
   984  		singleItemImplied:  isSingleItemImplied,
   985  		targetsSingleItems: true,
   986  	}
   987  
   988  	if len(b.resources) != 0 {
   989  		return result.withError(fmt.Errorf("you may not specify individual resources and bulk resources in the same call"))
   990  	}
   991  
   992  	// retrieve one client for each resource
   993  	mappings, err := b.resourceTupleMappings()
   994  	if err != nil {
   995  		result.err = err
   996  		return result
   997  	}
   998  	clients := make(map[string]RESTClient)
   999  	for _, mapping := range mappings {
  1000  		s := fmt.Sprintf("%s/%s", mapping.GroupVersionKind.GroupVersion().String(), mapping.Resource.Resource)
  1001  		if _, ok := clients[s]; ok {
  1002  			continue
  1003  		}
  1004  		client, err := b.getClient(mapping.GroupVersionKind.GroupVersion())
  1005  		if err != nil {
  1006  			result.err = err
  1007  			return result
  1008  		}
  1009  		clients[s] = client
  1010  	}
  1011  
  1012  	items := []Visitor{}
  1013  	for _, tuple := range b.resourceTuples {
  1014  		mapping, ok := mappings[tuple.Resource]
  1015  		if !ok {
  1016  			return result.withError(fmt.Errorf("resource %q is not recognized: %v", tuple.Resource, mappings))
  1017  		}
  1018  		s := fmt.Sprintf("%s/%s", mapping.GroupVersionKind.GroupVersion().String(), mapping.Resource.Resource)
  1019  		client, ok := clients[s]
  1020  		if !ok {
  1021  			return result.withError(fmt.Errorf("could not find a client for resource %q", tuple.Resource))
  1022  		}
  1023  
  1024  		selectorNamespace := b.namespace
  1025  		if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
  1026  			selectorNamespace = ""
  1027  		} else {
  1028  			if len(b.namespace) == 0 {
  1029  				errMsg := "namespace may not be empty when retrieving a resource by name"
  1030  				if b.allNamespace {
  1031  					errMsg = "a resource cannot be retrieved by name across all namespaces"
  1032  				}
  1033  				return result.withError(fmt.Errorf(errMsg))
  1034  			}
  1035  		}
  1036  
  1037  		info := &Info{
  1038  			Client:      client,
  1039  			Mapping:     mapping,
  1040  			Namespace:   selectorNamespace,
  1041  			Name:        tuple.Name,
  1042  			Subresource: b.subresource,
  1043  		}
  1044  		items = append(items, info)
  1045  	}
  1046  
  1047  	var visitors Visitor
  1048  	if b.continueOnError {
  1049  		visitors = EagerVisitorList(items)
  1050  	} else {
  1051  		visitors = VisitorList(items)
  1052  	}
  1053  	result.visitor = visitors
  1054  	result.sources = items
  1055  	return result
  1056  }
  1057  
  1058  func (b *Builder) visitByName() *Result {
  1059  	result := &Result{
  1060  		singleItemImplied:  len(b.names) == 1,
  1061  		targetsSingleItems: true,
  1062  	}
  1063  
  1064  	if len(b.paths) != 0 {
  1065  		return result.withError(fmt.Errorf("when paths, URLs, or stdin is provided as input, you may not specify a resource by arguments as well"))
  1066  	}
  1067  	if len(b.resources) == 0 {
  1068  		return result.withError(fmt.Errorf("you must provide a resource and a resource name together"))
  1069  	}
  1070  	if len(b.resources) > 1 {
  1071  		return result.withError(fmt.Errorf("you must specify only one resource"))
  1072  	}
  1073  
  1074  	mappings, err := b.resourceMappings()
  1075  	if err != nil {
  1076  		result.err = err
  1077  		return result
  1078  	}
  1079  	mapping := mappings[0]
  1080  
  1081  	client, err := b.getClient(mapping.GroupVersionKind.GroupVersion())
  1082  	if err != nil {
  1083  		result.err = err
  1084  		return result
  1085  	}
  1086  
  1087  	selectorNamespace := b.namespace
  1088  	if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
  1089  		selectorNamespace = ""
  1090  	} else {
  1091  		if len(b.namespace) == 0 {
  1092  			errMsg := "namespace may not be empty when retrieving a resource by name"
  1093  			if b.allNamespace {
  1094  				errMsg = "a resource cannot be retrieved by name across all namespaces"
  1095  			}
  1096  			return result.withError(fmt.Errorf(errMsg))
  1097  		}
  1098  	}
  1099  
  1100  	visitors := []Visitor{}
  1101  	for _, name := range b.names {
  1102  		info := &Info{
  1103  			Client:      client,
  1104  			Mapping:     mapping,
  1105  			Namespace:   selectorNamespace,
  1106  			Name:        name,
  1107  			Subresource: b.subresource,
  1108  		}
  1109  		visitors = append(visitors, info)
  1110  	}
  1111  	result.visitor = VisitorList(visitors)
  1112  	result.sources = visitors
  1113  	return result
  1114  }
  1115  
  1116  func (b *Builder) visitByPaths() *Result {
  1117  	result := &Result{
  1118  		singleItemImplied:  !b.dir && !b.stream && len(b.paths) == 1,
  1119  		targetsSingleItems: true,
  1120  	}
  1121  
  1122  	if len(b.resources) != 0 {
  1123  		return result.withError(fmt.Errorf("when paths, URLs, or stdin is provided as input, you may not specify resource arguments as well"))
  1124  	}
  1125  	if len(b.names) != 0 {
  1126  		return result.withError(fmt.Errorf("name cannot be provided when a path is specified"))
  1127  	}
  1128  	if len(b.resourceTuples) != 0 {
  1129  		return result.withError(fmt.Errorf("resource/name arguments cannot be provided when a path is specified"))
  1130  	}
  1131  
  1132  	var visitors Visitor
  1133  	if b.continueOnError {
  1134  		visitors = EagerVisitorList(b.paths)
  1135  	} else {
  1136  		visitors = ConcurrentVisitorList{
  1137  			visitors:    b.paths,
  1138  			concurrency: b.visitorConcurrency,
  1139  		}
  1140  	}
  1141  
  1142  	if b.flatten {
  1143  		visitors = NewFlattenListVisitor(visitors, b.objectTyper, b.mapper)
  1144  	}
  1145  
  1146  	// only items from disk can be refetched
  1147  	if b.latest {
  1148  		// must set namespace prior to fetching
  1149  		if b.defaultNamespace {
  1150  			visitors = NewDecoratedVisitor(visitors, SetNamespace(b.namespace))
  1151  		}
  1152  		visitors = NewDecoratedVisitor(visitors, RetrieveLatest)
  1153  	}
  1154  	if b.labelSelector != nil {
  1155  		selector, err := labels.Parse(*b.labelSelector)
  1156  		if err != nil {
  1157  			return result.withError(fmt.Errorf("the provided selector %q is not valid: %v", *b.labelSelector, err))
  1158  		}
  1159  		visitors = NewFilteredVisitor(visitors, FilterByLabelSelector(selector))
  1160  	}
  1161  	result.visitor = visitors
  1162  	result.sources = b.paths
  1163  	return result
  1164  }
  1165  
  1166  // Do returns a Result object with a Visitor for the resources identified by the Builder.
  1167  // The visitor will respect the error behavior specified by ContinueOnError. Note that stream
  1168  // inputs are consumed by the first execution - use Infos() or Object() on the Result to capture a list
  1169  // for further iteration.
  1170  func (b *Builder) Do() *Result {
  1171  	r := b.visitorResult()
  1172  	r.mapper = b.Mapper()
  1173  	if r.err != nil {
  1174  		return r
  1175  	}
  1176  	if b.flatten {
  1177  		r.visitor = NewFlattenListVisitor(r.visitor, b.objectTyper, b.mapper)
  1178  	}
  1179  	helpers := []VisitorFunc{}
  1180  	if b.defaultNamespace {
  1181  		helpers = append(helpers, SetNamespace(b.namespace))
  1182  	}
  1183  	if b.requireNamespace {
  1184  		helpers = append(helpers, RequireNamespace(b.namespace))
  1185  	}
  1186  	helpers = append(helpers, FilterNamespace)
  1187  	if b.requireObject {
  1188  		helpers = append(helpers, RetrieveLazy)
  1189  	}
  1190  	if b.continueOnError {
  1191  		r.visitor = ContinueOnErrorVisitor{Visitor: r.visitor}
  1192  	}
  1193  	r.visitor = NewDecoratedVisitor(r.visitor, helpers...)
  1194  	return r
  1195  }
  1196  
  1197  // SplitResourceArgument splits the argument with commas and returns unique
  1198  // strings in the original order.
  1199  func SplitResourceArgument(arg string) []string {
  1200  	out := []string{}
  1201  	set := sets.NewString()
  1202  	for _, s := range strings.Split(arg, ",") {
  1203  		if set.Has(s) {
  1204  			continue
  1205  		}
  1206  		set.Insert(s)
  1207  		out = append(out, s)
  1208  	}
  1209  	return out
  1210  }
  1211  
  1212  // HasNames returns true if the provided args contain resource names
  1213  func HasNames(args []string) (bool, error) {
  1214  	args = normalizeMultipleResourcesArgs(args)
  1215  	hasCombinedTypes, err := hasCombinedTypeArgs(args)
  1216  	if err != nil {
  1217  		return false, err
  1218  	}
  1219  	return hasCombinedTypes || len(args) > 1, nil
  1220  }
  1221  
  1222  // expandIfFilePattern returns all the filenames that match the input pattern
  1223  // or the filename if it is a specific filename and not a pattern.
  1224  // If the input is a pattern and it yields no result it will result in an error.
  1225  func expandIfFilePattern(pattern string) ([]string, error) {
  1226  	if _, err := os.Stat(pattern); os.IsNotExist(err) {
  1227  		matches, err := filepath.Glob(pattern)
  1228  		if err == nil && len(matches) == 0 {
  1229  			return nil, fmt.Errorf(pathNotExistError, pattern)
  1230  		}
  1231  		if err == filepath.ErrBadPattern {
  1232  			return nil, fmt.Errorf("pattern %q is not valid: %v", pattern, err)
  1233  		}
  1234  		return matches, err
  1235  	}
  1236  	return []string{pattern}, nil
  1237  }
  1238  
  1239  type cachingCategoryExpanderFunc struct {
  1240  	delegate CategoryExpanderFunc
  1241  
  1242  	lock   sync.Mutex
  1243  	cached restmapper.CategoryExpander
  1244  }
  1245  
  1246  func (c *cachingCategoryExpanderFunc) ToCategoryExpander() (restmapper.CategoryExpander, error) {
  1247  	c.lock.Lock()
  1248  	defer c.lock.Unlock()
  1249  	if c.cached != nil {
  1250  		return c.cached, nil
  1251  	}
  1252  
  1253  	ret, err := c.delegate()
  1254  	if err != nil {
  1255  		return nil, err
  1256  	}
  1257  	c.cached = ret
  1258  	return c.cached, nil
  1259  }
  1260  

View as plain text