resource目录下的概念们

在看Kubectl的源码时,最头痛的是/pkg/kubectl/resource目录下各种概念。什么是builder,什么是info,什么是visitor。。。如果了解了这些概念,再来看Kubectl源码,那么就能起到事半功倍的效果。本次先对Builder进行较深程度的分析,然后分批次介绍其他概念。概念的理解可能带有一些主观的色彩,还请包涵。

什么是Builder

Builder定义在/pkg/kubectl/resource/builder.go中。Builder是Kubectl命令行信息的内部载体,可以通过Builder生成Result对象(后续介绍)。Builder大多方法支持链式调用,即

1
2
3
4
5
6
7
8
9
10
resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.UnstructuredClientForMapping), runtime.UnstructuredJSONScheme).
NamespaceParam(cmdNamespace).DefaultNamespace().AllNamespaces(allNamespaces).
FilenameParam(enforceNamespace, &options.FilenameOptions).
SelectorParam(selector).
ExportParam(export).
ResourceTypeOrNameArgs(true, args...).
ContinueOnError().
Latest().
Flatten().
Do()

这是因为这些方法返回的对象自身。

先来看下Builder的定义。Builder中有一个mapper(后续介绍),然后就是一些资源方面的字段,这些资源字段都有方法对其进行设置。其中,resources需要和names或selector配合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Builder provides convenience functions for taking arguments and parameters
// from the command line and converting them to a list of resources to iterate
// over using the Visitor interface.
type Builder struct {
mapper *Mapper
errs []error
paths []Visitor
stream bool
dir bool
selector labels.Selector
selectAll bool
resources []string
namespace string
allNamespace bool
names []string
resourceTuples []resourceTuple
defaultNamespace bool
requireNamespace bool
flatten bool
latest bool
requireObject bool
singleResourceType bool
continueOnError bool
singular bool
export bool
schema validation.Schema
}

NewBuilder()

NewBuilder()生成一个Builder,新的Builder只设置了mapper,及标记requireObject为true。

1
2
3
4
5
6
7
func NewBuilder(mapper meta.RESTMapper, typer runtime.ObjectTyper, clientMapper ClientMapper, decoder runtime.Decoder) *Builder {
return &Builder{
//***把RESTMapper, ObjectTyper, ClientMapper, Decoder封装成kubectl的Mapper***//
mapper: &Mapper{typer, mapper, clientMapper, decoder},
requireObject: true,
}
}

Do()

Do()先调用visitorResult()生成Result对象,然后设置Result对象中的visitor。现在我们必需牢记,Builder的Do()可以生成Result。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//***返回一个*Result,Result可以使Infos()或Object()来获取执行结果***//
//***关于visitor层层封装,可以理解为每个visitor实现一定的功能,然后按一定的顺序来执行这些功能***//
func (b *Builder) Do() *Result {
r := b.visitorResult()
if r.err != nil {
return r
}
//***层层封装visitor***//
if b.flatten {
r.visitor = NewFlattenListVisitor(r.visitor, b.mapper)
}
helpers := []VisitorFunc{}
if b.defaultNamespace {
helpers = append(helpers, SetNamespace(b.namespace))
}
if b.requireNamespace {
helpers = append(helpers, RequireNamespace(b.namespace))
}
helpers = append(helpers, FilterNamespace)
if b.requireObject {
helpers = append(helpers, RetrieveLazy)
}
r.visitor = NewDecoratedVisitor(r.visitor, helpers...)
if b.continueOnError {
r.visitor = ContinueOnErrorVisitor{r.visitor}
}
return r
}

visitResult()

visitResult()会根据Builder中字段的设置情况来生成不同的Result。一般来说b.paths, b.selectors, b.resourceTuples, b.names只需设置一个即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//***返回一个result对象***//
//***visitByPaths()等会生成info对象***//
func (b *Builder) visitorResult() *Result {
if len(b.errs) > 0 {
return &Result{err: utilerrors.NewAggregate(b.errs)}
}
//***设置selector为labels.Everything()***//
if b.selectAll {
b.selector = labels.Everything()
}
// visit items specified by paths
if len(b.paths) != 0 {
return b.visitByPaths()
}
// visit selectors
if b.selector != nil {
return b.visitBySelector()
}
// visit items specified by resource and name
if len(b.resourceTuples) != 0 {
return b.visitByResource()
}
// visit items specified by name
if len(b.names) != 0 {
return b.visitByName()
}
if len(b.resources) != 0 {
return &Result{err: fmt.Errorf("resource(s) were provided, but no name, label selector, or --all flag specified")}
}
return &Result{err: missingResourceError}
}

visitBySelector()

visitBySelector()生成一个Selector visitor,然后把该visitor封装成Result并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//***selector visitor的visitFunc会生成info***//
func (b *Builder) visitBySelector() *Result {
if len(b.names) != 0 {
return &Result{err: fmt.Errorf("name cannot be provided when a selector is specified")}
}
if len(b.resourceTuples) != 0 {
return &Result{err: fmt.Errorf("selectors and the all flag cannot be used when passing resource/name arguments")}
}
if len(b.resources) == 0 {
return &Result{err: fmt.Errorf("at least one resource must be specified to use a selector")}
}
mappings, err := b.resourceMappings()
if err != nil {
return &Result{err: err}
}
visitors := []Visitor{}
for _, mapping := range mappings {
//***生成client***//
client, err := b.mapper.ClientForMapping(mapping)
if err != nil {
return &Result{err: err}
}
selectorNamespace := b.namespace
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
selectorNamespace = ""
}
visitors = append(visitors, NewSelector(client, mapping, selectorNamespace, b.selector, b.export))
}
if b.continueOnError {
return &Result{visitor: EagerVisitorList(visitors), sources: visitors}
}
return &Result{visitor: VisitorList(visitors), sources: visitors}
}

visitByResource()

visitByResource()使用resourceTuples生成Info(后续介绍),Info本身也是个visitor,所以把Info打包成Result并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//***通过resource生成info***//
func (b *Builder) visitByResource() *Result {
// if b.singular is false, this could be by default, so double-check length
// of resourceTuples to determine if in fact it is singular or not
isSingular := b.singular
if !isSingular {
isSingular = len(b.resourceTuples) == 1
}
if len(b.resources) != 0 {
return &Result{singular: isSingular, err: fmt.Errorf("you may not specify individual resources and bulk resources in the same call")}
}
// retrieve one client for each resource
mappings, err := b.resourceTupleMappings()
if err != nil {
return &Result{singular: isSingular, err: err}
}
clients := make(map[string]RESTClient)
for _, mapping := range mappings {
s := fmt.Sprintf("%s/%s", mapping.GroupVersionKind.GroupVersion().String(), mapping.Resource)
if _, ok := clients[s]; ok {
continue
}
client, err := b.mapper.ClientForMapping(mapping)
if err != nil {
return &Result{err: err}
}
clients[s] = client
}
items := []Visitor{}
for _, tuple := range b.resourceTuples {
mapping, ok := mappings[tuple.Resource]
if !ok {
return &Result{singular: isSingular, err: fmt.Errorf("resource %q is not recognized: %v", tuple.Resource, mappings)}
}
s := fmt.Sprintf("%s/%s", mapping.GroupVersionKind.GroupVersion().String(), mapping.Resource)
client, ok := clients[s]
if !ok {
return &Result{singular: isSingular, err: fmt.Errorf("could not find a client for resource %q", tuple.Resource)}
}
selectorNamespace := b.namespace
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
selectorNamespace = ""
} else {
if len(b.namespace) == 0 {
errMsg := "namespace may not be empty when retrieving a resource by name"
if b.allNamespace {
errMsg = "a resource cannot be retrieved by name across all namespaces"
}
return &Result{singular: isSingular, err: fmt.Errorf(errMsg)}
}
}
//***生成info***//
info := NewInfo(client, mapping, selectorNamespace, tuple.Name, b.export)
items = append(items, info)
}
var visitors Visitor
if b.continueOnError {
visitors = EagerVisitorList(items)
} else {
visitors = VisitorList(items)
}
return &Result{singular: isSingular, visitor: visitors, sources: items}
}

visitByName()

visitByName()先通过b.names来生成info,然后封装成Result返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//***kubectl get pods nginx-1487191267-47z76走此函数***//
//***通过resource name生成info***//
func (b *Builder) visitByName() *Result {
isSingular := len(b.names) == 1
if len(b.paths) != 0 {
return &Result{singular: isSingular, err: fmt.Errorf("when paths, URLs, or stdin is provided as input, you may not specify a resource by arguments as well")}
}
if len(b.resources) == 0 {
return &Result{singular: isSingular, err: fmt.Errorf("you must provide a resource and a resource name together")}
}
if len(b.resources) > 1 {
return &Result{singular: isSingular, err: fmt.Errorf("you must specify only one resource")}
}
mappings, err := b.resourceMappings()
if err != nil {
return &Result{singular: isSingular, err: err}
}
mapping := mappings[0]
client, err := b.mapper.ClientForMapping(mapping)
if err != nil {
return &Result{err: err}
}
selectorNamespace := b.namespace
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
selectorNamespace = ""
} else {
if len(b.namespace) == 0 {
errMsg := "namespace may not be empty when retrieving a resource by name"
if b.allNamespace {
errMsg = "a resource cannot be retrieved by name across all namespaces"
}
return &Result{singular: isSingular, err: fmt.Errorf(errMsg)}
}
}
visitors := []Visitor{}
for _, name := range b.names {
//***生成info***//
info := NewInfo(client, mapping, selectorNamespace, name, b.export)
visitors = append(visitors, info)
}
return &Result{singular: isSingular, visitor: VisitorList(visitors), sources: visitors}
}

visitByPaths()

visitByPaths()根据b.Paths生成Results。因为b.Paths本身就是个visitor数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//***Paths中的visitor会生成info***//
func (b *Builder) visitByPaths() *Result {
singular := !b.dir && !b.stream && len(b.paths) == 1
if len(b.resources) != 0 {
return &Result{singular: singular, err: fmt.Errorf("when paths, URLs, or stdin is provided as input, you may not specify resource arguments as well")}
}
if len(b.names) != 0 {
return &Result{err: fmt.Errorf("name cannot be provided when a path is specified")}
}
if len(b.resourceTuples) != 0 {
return &Result{err: fmt.Errorf("resource/name arguments cannot be provided when a path is specified")}
}
var visitors Visitor
if b.continueOnError {
//***此处path也是visitor***//
//***FileVisitor, StreamVisitor会生成info***//
visitors = EagerVisitorList(b.paths)
} else {
visitors = VisitorList(b.paths)
}
// only items from disk can be refetched
if b.latest {
// must flatten lists prior to fetching
if b.flatten {
visitors = NewFlattenListVisitor(visitors, b.mapper)
}
// must set namespace prior to fetching
if b.defaultNamespace {
visitors = NewDecoratedVisitor(visitors, SetNamespace(b.namespace))
}
visitors = NewDecoratedVisitor(visitors, RetrieveLatest)
}
if b.selector != nil {
visitors = NewFilteredVisitor(visitors, FilterBySelector(b.selector))
}
return &Result{singular: singular, visitor: visitors, sources: b.paths}
}

接下来介绍Builder的字段设置函数。

Schema()

Schema()设置Builder的schema字段。schema可以对资源名称进行规范检查。

1
2
3
4
func (b *Builder) Schema(schema validation.Schema) *Builder {
b.schema = schema
return b
}

FilenameParam()

FilenameParam()可以处理不同的输入方式,并设置Paths字段。如kubectl create -f abc.yaml,则filenameOptions为&{[/home/abc.yaml] false}。
FilenameParam()目前支持标准输入,URL方式,文件方式,对应的支持函数为Stdin(), URL(), Path()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func (b *Builder) FilenameParam(enforceNamespace bool, filenameOptions *FilenameOptions) *Builder {
recursive := filenameOptions.Recursive
paths := filenameOptions.Filenames
for _, s := range paths {
switch {
//***标准输入***//
case s == "-":
b.Stdin()
//***URL***//
case strings.Index(s, "http://") == 0 || strings.Index(s, "https://") == 0:
url, err := url.Parse(s)
if err != nil {
b.errs = append(b.errs, fmt.Errorf("the URL passed to filename %q is not valid: %v", s, err))
continue
}
b.URL(defaultHttpGetAttempts, url)
default:
if !recursive {
b.singular = true
}
//***文件或目录输入***//
b.Path(recursive, s)
}
}
//***如果enforceNamespace为true,则调用RequireNamespace()***//
if enforceNamespace {
b.RequireNamespace()
}
return b
}

Stdin()

Stdin()会调用FileVIsitorForSTDIN()生成一个visitor,然后把该visitor加入到paths中。

1
2
3
4
5
6
//***在builder的paths中加入stdin Visitor***//
func (b *Builder) Stdin() *Builder {
b.stream = true
b.paths = append(b.paths, FileVisitorForSTDIN(b.mapper, b.schema))
return b
}

URL()

URL()先生成一个url visitor,然后把该visitor加入到paths中。

1
2
3
4
5
6
7
8
9
10
11
//***在builder的paths中加入URL Visitor***//
func (b *Builder) URL(httpAttemptCount int, urls ...*url.URL) *Builder {
for _, u := range urls {
b.paths = append(b.paths, &URLVisitor{
URL: u,
StreamVisitor: NewStreamVisitor(nil, b.mapper, u.String(), b.schema),
HttpAttemptCount: httpAttemptCount,
})
}
return b
}

Stream()

Stream()先生成一个stream visitor,然后把该visitor加入到paths中。

1
2
3
4
5
6
//***在builder的paths中加入stream Visitor***//
func (b *Builder) Stream(r io.Reader, name string) *Builder {
b.stream = true
b.paths = append(b.paths, NewStreamVisitor(r, b.mapper, name, b.schema))
return b
}

Path()

Path()把路径转换成file visitor,然后加入到paths中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//***把path转换成file Visitor,然后加入到paths中***//
func (b *Builder) Path(recursive bool, paths ...string) *Builder {
for _, p := range paths {
_, err := os.Stat(p)
if os.IsNotExist(err) {
b.errs = append(b.errs, fmt.Errorf("the path %q does not exist", p))
continue
}
if err != nil {
b.errs = append(b.errs, fmt.Errorf("the path %q cannot be accessed: %v", p, err))
continue
}
//***把paths转换成file Visitors***//
visitors, err := ExpandPathsToFileVisitors(b.mapper, p, recursive, FileExtensions, b.schema)
if err != nil {
b.errs = append(b.errs, fmt.Errorf("error reading %q: %v", p, err))
}
if len(visitors) > 1 {
b.dir = true
}
//***加入到paths中***//
b.paths = append(b.paths, visitors...)
}
return b
}

ResourceTypes()

ResourceTypes()设置Builder的resources字段,如kubectl get pods abc,则resources为pods

1
2
3
4
5
6
//***设置resources***//
//***builder resources中存储的是需要处理的类型***//
func (b *Builder) ResourceTypes(types ...string) *Builder {
b.resources = append(b.resources, types...)
return b
}

ResourceNames()

ResourceNames()设置resourceTuples字段,表示需要访问的对象。如果kubectl logs nginx,那么ResourceNames()会把”nginx”和Builder中的resource(logs命令中设置为pods),组成(pods, nginx)加入到resourceTuples中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (b *Builder) ResourceNames(resource string, names ...string) *Builder {
for _, name := range names {
// See if this input string is of type/name format
tuple, ok, err := splitResourceTypeName(name)
if err != nil {
b.errs = append(b.errs, err)
return b
}
if ok {
b.resourceTuples = append(b.resourceTuples, tuple)
continue
}
if len(resource) == 0 {
b.errs = append(b.errs, fmt.Errorf("the argument %q must be RESOURCE/NAME", name))
continue
}
// Use the given default type to create a resource tuple
b.resourceTuples = append(b.resourceTuples, resourceTuple{Resource: resource, Name: name})
}
return b
}

SelectorParam()

SelectorParam()的参数为string,设置Builder的selector字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//***设置selector***//
func (b *Builder) SelectorParam(s string) *Builder {
selector, err := labels.Parse(s)
if err != nil {
b.errs = append(b.errs, fmt.Errorf("the provided selector %q is not valid: %v", s, err))
return b
}
if selector.Empty() {
return b
}
if b.selectAll {
b.errs = append(b.errs, fmt.Errorf("found non empty selector %q with previously set 'all' parameter. ", s))
return b
}
return b.Selector(selector)
}

Selector()

Selector()的参数为selector,直接设置Builder的selector字段。

1
2
3
4
func (b *Builder) Selector(selector labels.Selector) *Builder {
b.selector = selector
return b
}

ExportParam()

用途未知。

1
2
3
4
func (b *Builder) ExportParam(export bool) *Builder {
b.export = export
return b
}

NamespaceParam()

NamespaceParam()可以设置Builder的namespace字段。

1
2
3
4
5
//***设置namespace***//
func (b *Builder) NamespaceParam(namespace string) *Builder {
b.namespace = namespace
return b
}

DefaultNamespace()

DefaultNamespace()可以设置Builder的defaultNamespace字段,表示如果namespace未指定,是否使用default namespace。

1
2
3
4
5
//***设置DefaultNamespace***//
func (b *Builder) DefaultNamespace() *Builder {
b.defaultNamespace = true
return b
}

AllNamespaces()

AllNamespaces()可以设置Builder的allNamespace和namespace字段,表示访问NamespaceAll。

1
2
3
4
5
6
7
8
//***把namespace设置为NamespaceAll***//
func (b *Builder) AllNamespaces(allNamespace bool) *Builder {
if allNamespace {
b.namespace = api.NamespaceAll
}
b.allNamespace = allNamespace
return b
}

RequireNamespace()

RequireNamespace()可以设置Builder的requireNamespace字段。

1
2
3
4
func (b *Builder) RequireNamespace() *Builder {
b.requireNamespace = true
return b
}

SelectAllParam()

SelectAllParam()可以设置Builder的selectAll字段。与selector字段互斥。

1
2
3
4
5
6
7
8
func (b *Builder) SelectAllParam(selectAll bool) *Builder {
if selectAll && b.selector != nil {
b.errs = append(b.errs, fmt.Errorf("setting 'all' parameter but found a non empty selector. "))
return b
}
b.selectAll = selectAll
return b
}

ResourceTypeOrNameArgs()

ResourceTypeOrNameArgs()可以设置Builder的names和resource字段。如执行kubectl get pods/nginx-1487191267-b4w5j rc/abc,则args为[pods/nginx-1487191267-b4w5j rc/abc]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func (b *Builder) ResourceTypeOrNameArgs(allowEmptySelector bool, args ...string) *Builder {
args = normalizeMultipleResourcesArgs(args)
if ok, err := hasCombinedTypeArgs(args); ok {
if err != nil {
b.errs = append(b.errs, err)
return b
}
for _, s := range args {
tuple, ok, err := splitResourceTypeName(s)
if err != nil {
b.errs = append(b.errs, err)
return b
}
if ok {
b.resourceTuples = append(b.resourceTuples, tuple)
}
}
return b
}
if len(args) > 0 {
// Try replacing aliases only in types
args[0] = b.ReplaceAliases(args[0])
}
//***此处调用ResourceTypes()***//
switch {
case len(args) > 2:
b.names = append(b.names, args[1:]...)
b.ResourceTypes(SplitResourceArgument(args[0])...)
case len(args) == 2:
b.names = append(b.names, args[1])
b.ResourceTypes(SplitResourceArgument(args[0])...)
case len(args) == 1:
b.ResourceTypes(SplitResourceArgument(args[0])...)
if b.selector == nil && allowEmptySelector {
b.selector = labels.Everything()
}
case len(args) == 0:
default:
b.errs = append(b.errs, fmt.Errorf("arguments must consist of a resource or a resource and name"))
}
return b
}

Flatten()

Flatten()可以设置Builder的flatten字段。

1
2
3
4
func (b *Builder) Flatten() *Builder {
b.flatten = true
return b
}

Latest()

Latest()可以设置Builder的latest字段,用来标识获取URL或Path中最新的内容。

1
2
3
4
func (b *Builder) Latest() *Builder {
b.latest = true
return b
}

RequireObject()

RequireObject()可以设置Builder的requireObject字段。

1
2
3
4
func (b *Builder) RequireObject(require bool) *Builder {
b.requireObject = require
return b
}

ContinueOnError()

ContinueOnError()可以设置Builder的continueOnError字段为true,在封装visitor时会用到该字段。

1
2
3
4
func (b *Builder) ContinueOnError() *Builder {
b.continueOnError = true
return b
}

mappingFor()

mappingFor()使用参数resource,然后依据mapper字段,构建RESTMapping并返回。RESTMapping表示resource和GVK的对应关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//***返回RESTMapping***//
func (b *Builder) mappingFor(resourceArg string) (*meta.RESTMapping, error) {
//***把resource构造成GVR***//
fullySpecifiedGVR, groupResource := unversioned.ParseResourceArg(resourceArg)
gvk := unversioned.GroupVersionKind{}
if fullySpecifiedGVR != nil {
gvk, _ = b.mapper.KindFor(*fullySpecifiedGVR)
}
if gvk.Empty() {
var err error
gvk, err = b.mapper.KindFor(groupResource.WithVersion(""))
if err != nil {
return nil, err
}
}
//***RESTMapping()定义在/pkg/api/meta/restmapper.go中***//
return b.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
}

resourceMappings()

resourceMappings()把Builder中的resource传递给上面的mappingFor()生成RESTMapping。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//***把builder中resources转换成mapping***//
func (b *Builder) resourceMappings() ([]*meta.RESTMapping, error) {
if len(b.resources) > 1 && b.singleResourceType {
return nil, fmt.Errorf("you may only specify a single resource type")
}
mappings := []*meta.RESTMapping{}
for _, r := range b.resources {
mapping, err := b.mappingFor(r)
if err != nil {
return nil, err
}
mappings = append(mappings, mapping)
}
return mappings, nil
}

resourceTupleMappings()

resourceTupleMappings()把Builder中的resourceTuple转换成RESTMapping。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (b *Builder) resourceTupleMappings() (map[string]*meta.RESTMapping, error) {
mappings := make(map[string]*meta.RESTMapping)
canonical := make(map[string]struct{})
for _, r := range b.resourceTuples {
if _, ok := mappings[r.Resource]; ok {
continue
}
mapping, err := b.mappingFor(r.Resource)
if err != nil {
return nil, err
}
mappings[mapping.Resource] = mapping
mappings[r.Resource] = mapping
canonical[mapping.Resource] = struct{}{}
}
if len(canonical) > 1 && b.singleResourceType {
return nil, fmt.Errorf("you may only specify a single resource type")
}
return mappings, nil
}