Merge pull request #5903 from smarterclayton/support_resources_by_type_and_name

Allow resource.Builder commands to take arguments by type/name
This commit is contained in:
Clayton Coleman
2015-03-26 14:23:03 -04:00
16 changed files with 343 additions and 46 deletions

View File

@@ -50,6 +50,8 @@ type Builder struct {
namespace string
names []string
resourceTuples []resourceTuple
defaultNamespace bool
requireNamespace bool
@@ -60,6 +62,11 @@ type Builder struct {
continueOnError bool
}
type resourceTuple struct {
Resource string
Name string
}
// NewBuilder creates a builder that operates on generic objects.
func NewBuilder(mapper meta.RESTMapper, typer runtime.ObjectTyper, clientMapper ClientMapper) *Builder {
return &Builder{
@@ -223,6 +230,26 @@ func (b *Builder) SelectAllParam(selectAll bool) *Builder {
// When two or more arguments are received, they must be a single type and resource name(s).
// The allowEmptySelector permits to select all the resources (via Everything func).
func (b *Builder) ResourceTypeOrNameArgs(allowEmptySelector bool, args ...string) *Builder {
if ok, err := hasCombinedTypeArgs(args); ok {
if err != nil {
b.errs = append(b.errs, err)
return b
}
for _, s := range args {
seg := strings.Split(s, "/")
if len(seg) != 2 {
b.errs = append(b.errs, fmt.Errorf("arguments in resource/name form may not have more than one slash"))
return b
}
resource, name := seg[0], seg[1]
if len(resource) == 0 || len(name) == 0 || len(SplitResourceArgument(resource)) != 1 {
b.errs = append(b.errs, fmt.Errorf("arguments in resource/name form must have a single resource and name"))
return b
}
b.resourceTuples = append(b.resourceTuples, resourceTuple{Resource: resource, Name: name})
}
return b
}
switch {
case len(args) > 2:
b.names = append(b.names, args[1:]...)
@@ -242,6 +269,23 @@ func (b *Builder) ResourceTypeOrNameArgs(allowEmptySelector bool, args ...string
return b
}
func hasCombinedTypeArgs(args []string) (bool, error) {
hasSlash := 0
for _, s := range args {
if strings.Contains(s, "/") {
hasSlash++
}
}
switch {
case hasSlash > 0 && hasSlash == len(args):
return true, nil
case hasSlash > 0 && hasSlash != len(args):
return true, fmt.Errorf("when passing arguments in resource/name form, all arguments must include the resource")
default:
return false, nil
}
}
// ResourceTypeAndNameArgs expects two arguments, a resource type, and a resource name. The resource
// matching that type and and name will be retrieved from the server.
func (b *Builder) ResourceTypeAndNameArgs(args ...string) *Builder {
@@ -304,6 +348,31 @@ func (b *Builder) resourceMappings() ([]*meta.RESTMapping, error) {
return mappings, nil
}
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
}
version, kind, err := b.mapper.VersionAndKindForResource(r.Resource)
if err != nil {
return nil, err
}
mapping, err := b.mapper.RESTMapping(kind, version)
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
}
func (b *Builder) visitorResult() *Result {
if len(b.errs) > 0 {
return &Result{err: errors.NewAggregate(b.errs)}
@@ -318,6 +387,9 @@ func (b *Builder) visitorResult() *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")}
}
@@ -352,6 +424,69 @@ func (b *Builder) visitorResult() *Result {
return &Result{visitor: VisitorList(visitors), sources: visitors}
}
// visit items specified by resource and name
if len(b.resourceTuples) != 0 {
isSingular := len(b.resourceTuples) == 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 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.APIVersion, 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.APIVersion, 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 {
return &Result{singular: isSingular, err: fmt.Errorf("namespace may not be empty when retrieving a resource by name")}
}
}
info := NewInfo(client, mapping, selectorNamespace, tuple.Name)
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}
}
// visit items specified by name
if len(b.names) != 0 {
isSingular := len(b.names) == 1
@@ -444,7 +579,10 @@ func (b *Builder) Do() *Result {
if b.requireNamespace {
helpers = append(helpers, RequireNamespace(b.namespace))
}
helpers = append(helpers, FilterNamespace())
helpers = append(helpers, FilterNamespace)
if b.latest {
helpers = append(helpers, RetrieveLazy)
}
r.visitor = NewDecoratedVisitor(r.visitor, helpers...)
return r
}

View File

@@ -416,6 +416,88 @@ func TestSingleResourceType(t *testing.T) {
}
}
func TestResourceTuple(t *testing.T) {
expectNoErr := func(err error) bool { return err == nil }
expectErr := func(err error) bool { return err != nil }
testCases := map[string]struct {
args []string
errFn func(error) bool
}{
"valid": {
args: []string{"pods/foo"},
errFn: expectNoErr,
},
"valid multiple with name indirection": {
args: []string{"pods/foo", "pod/bar"},
errFn: expectNoErr,
},
"valid multiple with namespaced and non-namespaced types": {
args: []string{"minions/foo", "pod/bar"},
errFn: expectNoErr,
},
"mixed arg types": {
args: []string{"pods/foo", "bar"},
errFn: expectErr,
},
/*"missing resource": {
args: []string{"pods/foo2"},
errFn: expectNoErr, // not an error because resources are lazily visited
},*/
"comma in resource": {
args: []string{",pods/foo"},
errFn: expectErr,
},
"multiple types in resource": {
args: []string{"pods,services/foo"},
errFn: expectErr,
},
"unknown resource type": {
args: []string{"unknown/foo"},
errFn: expectErr,
},
"leading slash": {
args: []string{"/bar"},
errFn: expectErr,
},
"trailing slash": {
args: []string{"bar/"},
errFn: expectErr,
},
}
for k, testCase := range testCases {
pods, _ := testData()
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{
"/namespaces/test/pods/foo": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]),
"/namespaces/test/pods/bar": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]),
"/nodes/foo": runtime.EncodeOrDie(latest.Codec, &api.Node{ObjectMeta: api.ObjectMeta{Name: "foo"}}),
})).
NamespaceParam("test").DefaultNamespace().
ResourceTypeOrNameArgs(true, testCase.args...)
r := b.Do()
if !testCase.errFn(r.Err()) {
t.Errorf("%s: unexpected error: %v", k, r.Err())
}
if r.Err() != nil {
continue
}
switch {
case (r.singular && len(testCase.args) != 1),
(!r.singular && len(testCase.args) == 1):
t.Errorf("%s: result had unexpected singular value", k)
}
info, err := r.Infos()
if err != nil {
// test error
continue
}
if len(info) != len(testCase.args) {
t.Errorf("%s: unexpected number of infos returned: %#v", info)
}
}
}
func TestStream(t *testing.T) {
r, pods, rc := streamTestData()
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
@@ -619,7 +701,7 @@ func TestLatest(t *testing.T) {
err := b.Do().IntoSingular(&singular).Visit(test.Handle)
if err != nil || singular || len(test.Infos) != 3 {
t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos)
t.Fatalf("unexpected response: %v %t %#v", err, singular, test.Infos)
}
if !api.Semantic.DeepDerivative([]runtime.Object{newPod, newPod2, newSvc}, test.Objects()) {
t.Errorf("unexpected visited objects: %#v", test.Objects())

View File

@@ -130,6 +130,11 @@ func (i *Info) Refresh(obj runtime.Object, ignoreError bool) error {
return nil
}
// Namespaced returns true if the object belongs to a namespace
func (i *Info) Namespaced() bool {
return i.Mapping != nil && i.Mapping.Scope.Name() == meta.RESTScopeNameNamespace
}
// Watch returns server changes to this object after it was retrieved.
func (i *Info) Watch(resourceVersion string) (watch.Interface, error) {
return NewHelper(i.Client, i.Mapping).WatchSingle(i.Namespace, i.Name, resourceVersion)
@@ -418,14 +423,12 @@ func UpdateObjectNamespace(info *Info) error {
}
// FilterNamespace omits the namespace if the object is not namespace scoped
func FilterNamespace() VisitorFunc {
return func(info *Info) error {
if info.Mapping.Scope.Name() != meta.RESTScopeNameNamespace {
info.Namespace = ""
UpdateObjectNamespace(info)
}
return nil
func FilterNamespace(info *Info) error {
if !info.Namespaced() {
info.Namespace = ""
UpdateObjectNamespace(info)
}
return nil
}
// SetNamespace ensures that every Info object visited will have a namespace
@@ -446,6 +449,9 @@ func SetNamespace(namespace string) VisitorFunc {
// accidentally operating on resources outside their namespace.
func RequireNamespace(namespace string) VisitorFunc {
return func(info *Info) error {
if !info.Namespaced() {
return nil
}
if len(info.Namespace) == 0 {
info.Namespace = namespace
UpdateObjectNamespace(info)
@@ -461,9 +467,12 @@ func RequireNamespace(namespace string) VisitorFunc {
// RetrieveLatest updates the Object on each Info by invoking a standard client
// Get.
func RetrieveLatest(info *Info) error {
if len(info.Name) == 0 || len(info.Namespace) == 0 {
if len(info.Name) == 0 {
return nil
}
if info.Namespaced() && len(info.Namespace) == 0 {
return fmt.Errorf("no namespace set on resource %s %q", info.Mapping.Resource, info.Name)
}
obj, err := NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name)
if err != nil {
return err
@@ -472,3 +481,11 @@ func RetrieveLatest(info *Info) error {
info.ResourceVersion, _ = info.Mapping.MetadataAccessor.ResourceVersion(obj)
return nil
}
// RetrieveLazy updates the object if it has not been loaded yet.
func RetrieveLazy(info *Info) error {
if info.Object == nil {
return info.Get()
}
return nil
}