Merge pull request #5903 from smarterclayton/support_resources_by_type_and_name

Allow resource.Builder commands to take arguments by type/name
pull/6/head
Clayton Coleman 2015-03-26 14:23:03 -04:00
commit 1e496696ca
16 changed files with 343 additions and 46 deletions

View File

@ -8,13 +8,13 @@ Display one or many resources
Display one or many resources.
Possible resources include pods (po), replication controllers (rc), services
(se), minions (mi), or events (ev).
(svc), minions (mi), or events (ev).
By specifying the output as 'template' and providing a Go template as the value
of the --template flag, you can filter the attributes of the fetched resource(s).
```
kubectl get [(-o|--output=)json|yaml|template|...] RESOURCE [ID]
kubectl get [(-o|--output=)json|yaml|template|...] (RESOURCE [NAME] | RESOURCE/NAME ...)
```
### Examples
@ -23,17 +23,20 @@ kubectl get [(-o|--output=)json|yaml|template|...] RESOURCE [ID]
// List all pods in ps output format.
$ kubectl get pods
// List a single replication controller with specified ID in ps output format.
$ kubectl get replicationController 1234-56-7890-234234-456456
// List a single replication controller with specified NAME in ps output format.
$ kubectl get replicationController web
// List a single pod in JSON output format.
$ kubectl get -o json pod 1234-56-7890-234234-456456
$ kubectl get -o json pod web-pod-13je7
// Return only the status value of the specified pod.
$ kubectl get -o template pod 1234-56-7890-234234-456456 --template={{.currentState.status}}
$ kubectl get -o template web-pod-13je7 --template={{.currentState.status}}
// List all replication controllers and services together in ps output format.
$ kubectl get rc,services
// List one or more resources by their type and names
$ kubectl get rc/web service/frontend pods/web-pod-13je7
```
### Options

View File

@ -17,7 +17,7 @@ Display one or many resources.
.PP
Possible resources include pods (po), replication controllers (rc), services
(se), minions (mi), or events (ev).
(svc), minions (mi), or events (ev).
.PP
By specifying the output as 'template' and providing a Go template as the value
@ -169,18 +169,21 @@ of the \-\-template flag, you can filter the attributes of the fetched resource(
// List all pods in ps output format.
$ kubectl get pods
// List a single replication controller with specified ID in ps output format.
$ kubectl get replicationController 1234\-56\-7890\-234234\-456456
// List a single replication controller with specified NAME in ps output format.
$ kubectl get replicationController web
// List a single pod in JSON output format.
$ kubectl get \-o json pod 1234\-56\-7890\-234234\-456456
$ kubectl get \-o json pod web\-pod\-13je7
// Return only the status value of the specified pod.
$ kubectl get \-o template pod 1234\-56\-7890\-234234\-456456 \-\-template=\{\{.currentState.status\}\}
$ kubectl get \-o template web\-pod\-13je7 \-\-template=\{\{.currentState.status\}\}
// List all replication controllers and services together in ps output format.
$ kubectl get rc,services
// List one or more resources by their type and names
$ kubectl get rc/web service/frontend pods/web\-pod\-13je7
.fi
.RE

View File

@ -155,6 +155,8 @@ for version in "${kube_api_versions[@]}"; do
# Post-condition: valid-pod POD is running
kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:'
kube::test::get_object_assert 'pod valid-pod' "{{.$id_field}}" 'valid-pod'
kube::test::get_object_assert 'pod/valid-pod' "{{.$id_field}}" 'valid-pod'
kube::test::get_object_assert 'pods/valid-pod' "{{.$id_field}}" 'valid-pod'
# Describe command should print detailed information
kube::test::describe_object_assert pods 'valid-pod' "Name:" "Image(s):" "Host:" "Labels:" "Status:" "Replication Controllers"
@ -524,6 +526,13 @@ __EOF__
kube::test::describe_object_assert minions "127.0.0.1" "Name:" "Conditions:" "Addresses:" "Capacity:" "Pods:"
fi
#####################
# Retrieve multiple #
#####################
kube::log::status "Testing kubectl(${version}:multiget)"
kube::test::get_object_assert 'nodes/127.0.0.1 service/kubernetes' "{{range.items}}{{.$id_field}}:{{end}}" '127.0.0.1:kubernetes:'
kube::test::clear_all
done

View File

@ -99,7 +99,7 @@ func RunCreate(f *Factory, out io.Writer, cmd *cobra.Command, filenames util.Str
}
count++
info.Refresh(obj, true)
fmt.Fprintf(out, "%s\n", info.Name)
fmt.Fprintf(out, "%s/%s\n", info.Mapping.Resource, info.Name)
return nil
})
if err != nil {

View File

@ -61,7 +61,7 @@ func TestCreateObject(t *testing.T) {
cmd.Run(cmd, []string{})
// uses the name from the file, not the response
if buf.String() != "redis-master-controller\n" {
if buf.String() != "replicationControllers/redis-master-controller\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}
@ -94,7 +94,7 @@ func TestCreateMultipleObject(t *testing.T) {
cmd.Run(cmd, []string{})
// Names should come from the REST response, NOT the files
if buf.String() != "rc1\nbaz\n" {
if buf.String() != "replicationControllers/rc1\nservices/baz\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}
@ -126,7 +126,7 @@ func TestCreateDirectory(t *testing.T) {
cmd.Flags().Set("filename", "../../../examples/guestbook")
cmd.Run(cmd, []string{})
if buf.String() != "name\nbaz\nname\nbaz\nname\nbaz\n" {
if buf.String() != "replicationControllers/name\nservices/baz\nreplicationControllers/name\nservices/baz\nreplicationControllers/name\nservices/baz\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}

View File

@ -99,7 +99,7 @@ func RunDelete(f *Factory, out io.Writer, cmd *cobra.Command, args []string, fil
if err := resource.NewHelper(r.Client, r.Mapping).Delete(r.Namespace, r.Name); err != nil {
return err
}
fmt.Fprintf(out, "%s\n", r.Name)
fmt.Fprintf(out, "%s/%s\n", r.Mapping.Resource, r.Name)
return nil
})
if err != nil {

View File

@ -50,7 +50,7 @@ func TestDeleteObject(t *testing.T) {
cmd.Run(cmd, []string{})
// uses the name from the file, not the response
if buf.String() != "redis-master-controller\n" {
if buf.String() != "replicationControllers/redis-master-controller\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}
@ -109,7 +109,7 @@ func TestDeleteMultipleObject(t *testing.T) {
cmd.Flags().Set("filename", "../../../examples/guestbook/frontend-service.json")
cmd.Run(cmd, []string{})
if buf.String() != "redis-master-controller\nfrontend\n" {
if buf.String() != "replicationControllers/redis-master-controller\nservices/frontend\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}
@ -141,7 +141,7 @@ func TestDeleteMultipleObjectIgnoreMissing(t *testing.T) {
cmd.Flags().Set("filename", "../../../examples/guestbook/frontend-service.json")
cmd.Run(cmd, []string{})
if buf.String() != "frontend\n" {
if buf.String() != "services/frontend\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}
@ -172,7 +172,7 @@ func TestDeleteDirectory(t *testing.T) {
cmd.Flags().Set("filename", "../../../examples/guestbook")
cmd.Run(cmd, []string{})
if buf.String() != "frontend-controller\nfrontend\nredis-master-controller\nredis-master\nredis-slave-controller\nredis-slave\n" {
if buf.String() != "replicationControllers/frontend-controller\nservices/frontend\nreplicationControllers/redis-master-controller\nservices/redis-master\nreplicationControllers/redis-slave-controller\nservices/redis-slave\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}
@ -213,7 +213,7 @@ func TestDeleteMultipleSelector(t *testing.T) {
cmd.Flags().Set("selector", "a=b")
cmd.Run(cmd, []string{"pods,services"})
if buf.String() != "foo\nbar\nbaz\n" {
if buf.String() != "pods/foo\npods/bar\nservices/baz\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}

View File

@ -34,31 +34,34 @@ const (
get_long = `Display one or many resources.
Possible resources include pods (po), replication controllers (rc), services
(se), minions (mi), or events (ev).
(svc), minions (mi), or events (ev).
By specifying the output as 'template' and providing a Go template as the value
of the --template flag, you can filter the attributes of the fetched resource(s).`
get_example = `// List all pods in ps output format.
$ kubectl get pods
// List a single replication controller with specified ID in ps output format.
$ kubectl get replicationController 1234-56-7890-234234-456456
// List a single replication controller with specified NAME in ps output format.
$ kubectl get replicationController web
// List a single pod in JSON output format.
$ kubectl get -o json pod 1234-56-7890-234234-456456
$ kubectl get -o json pod web-pod-13je7
// Return only the status value of the specified pod.
$ kubectl get -o template pod 1234-56-7890-234234-456456 --template={{.currentState.status}}
$ kubectl get -o template web-pod-13je7 --template={{.currentState.status}}
// List all replication controllers and services together in ps output format.
$ kubectl get rc,services`
$ kubectl get rc,services
// List one or more resources by their type and names
$ kubectl get rc/web service/frontend pods/web-pod-13je7`
)
// NewCmdGet creates a command object for the generic "get" action, which
// retrieves one or more resources from a server.
func (f *Factory) NewCmdGet(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "get [(-o|--output=)json|yaml|template|...] RESOURCE [ID]",
Use: "get [(-o|--output=)json|yaml|template|...] (RESOURCE [NAME] | RESOURCE/NAME ...)",
Short: "Display one or many resources",
Long: get_long,
Example: get_example,
@ -141,6 +144,7 @@ func RunGet(f *Factory, out io.Writer, cmd *cobra.Command, args []string) error
NamespaceParam(cmdNamespace).DefaultNamespace().
SelectorParam(selector).
ResourceTypeOrNameArgs(true, args...).
ContinueOnError().
Latest()
printer, generic, err := util.PrinterForCommand(cmd)
if err != nil {

View File

@ -30,6 +30,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json"
)
@ -313,6 +314,47 @@ func TestGetMultipleTypeObjectsWithSelector(t *testing.T) {
}
}
func TestGetMultipleTypeObjectsWithDirectReference(t *testing.T) {
_, svc, _ := testData()
node := &api.Node{
ObjectMeta: api.ObjectMeta{
Name: "foo",
},
}
f, tf, codec := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &client.FakeRESTClient{
Codec: codec,
Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) {
switch req.URL.Path {
case "/nodes/foo":
return &http.Response{StatusCode: 200, Body: objBody(codec, node)}, nil
case "/namespaces/test/services/bar":
return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
cmd := f.NewCmdGet(buf)
cmd.SetOutput(buf)
cmd.Run(cmd, []string{"services/bar", "node/foo"})
expected := []runtime.Object{&svc.Items[0], node}
actual := tf.Printer.(*testPrinter).Objects
if !api.Semantic.DeepEqual(expected, actual) {
t.Errorf("unexpected object: %s", util.ObjectDiff(expected, actual))
}
if len(buf.String()) == 0 {
t.Errorf("unexpected empty output")
}
}
func watchTestData() ([]api.Pod, []watch.Event) {
pods := []api.Pod{
{

View File

@ -71,11 +71,10 @@ func (f *Factory) NewCmdStop(out io.Writer) *cobra.Command {
r.Visit(func(info *resource.Info) error {
reaper, err := f.Reaper(info.Mapping)
cmdutil.CheckErr(err)
s, err := reaper.Stop(info.Namespace, info.Name)
if err != nil {
if _, err := reaper.Stop(info.Namespace, info.Name); err != nil {
return err
}
fmt.Fprintf(out, "%s\n", s)
fmt.Fprintf(out, "%s/%s\n", info.Mapping.Resource, info.Name)
return nil
})
},

View File

@ -111,7 +111,7 @@ func RunUpdate(f *Factory, out io.Writer, cmd *cobra.Command, args []string, fil
return err
}
info.Refresh(obj, true)
fmt.Fprintf(out, "%s\n", info.Name)
fmt.Fprintf(out, "%s/%s\n", info.Mapping.Resource, info.Name)
return nil
})

View File

@ -52,7 +52,7 @@ func TestUpdateObject(t *testing.T) {
cmd.Run(cmd, []string{})
// uses the name from the file, not the response
if buf.String() != "rc1\n" {
if buf.String() != "replicationControllers/rc1\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}
@ -88,7 +88,7 @@ func TestUpdateMultipleObject(t *testing.T) {
cmd.Flags().Set("filename", "../../../examples/guestbook/frontend-service.json")
cmd.Run(cmd, []string{})
if buf.String() != "rc1\nbaz\n" {
if buf.String() != "replicationControllers/rc1\nservices/baz\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}
@ -120,7 +120,7 @@ func TestUpdateDirectory(t *testing.T) {
cmd.Flags().Set("namespace", "test")
cmd.Run(cmd, []string{})
if buf.String() != "rc1\nbaz\nrc1\nbaz\nrc1\nbaz\n" {
if buf.String() != "replicationControllers/rc1\nservices/baz\nreplicationControllers/rc1\nservices/baz\nreplicationControllers/rc1\nservices/baz\n" {
t.Errorf("unexpected output: %s", buf.String())
}
}

View File

@ -49,7 +49,7 @@ func CheckErr(err error) {
if client.IsUnexpectedStatusError(err) {
glog.FatalDepth(1, fmt.Sprintf("Unexpected status received from server: %s", err.Error()))
}
glog.FatalDepth(1, fmt.Sprintf("Client error processing command: %s", err.Error()))
glog.FatalDepth(1, fmt.Sprintf("Error: %s", err.Error()))
}
}

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 {
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
}