mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
Move Resource functionality to its own package
Create a unified Builder object for working with files, selectors, types, and items that makes it easier to get multi-object functionality. Supports all of the behaviors previously in resource.go, but with additional flexibility to allow multi-type retrieval and access, directories, URLs, nested objects, and lists.
This commit is contained in:
parent
68298f08a4
commit
d75a3d5021
@ -105,6 +105,7 @@ func FirstNonEmptyString(args ...string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return a list of file names of a certain type within a given directory.
|
// Return a list of file names of a certain type within a given directory.
|
||||||
|
// TODO: replace with resource.Builder
|
||||||
func GetFilesFromDir(directory string, fileType string) []string {
|
func GetFilesFromDir(directory string, fileType string) []string {
|
||||||
files := []string{}
|
files := []string{}
|
||||||
|
|
||||||
@ -121,6 +122,7 @@ func GetFilesFromDir(directory string, fileType string) []string {
|
|||||||
|
|
||||||
// ReadConfigData reads the bytes from the specified filesytem or network
|
// ReadConfigData reads the bytes from the specified filesytem or network
|
||||||
// location or from stdin if location == "-".
|
// location or from stdin if location == "-".
|
||||||
|
// TODO: replace with resource.Builder
|
||||||
func ReadConfigData(location string) ([]byte, error) {
|
func ReadConfigData(location string) ([]byte, error) {
|
||||||
if len(location) == 0 {
|
if len(location) == 0 {
|
||||||
return nil, fmt.Errorf("location given but empty")
|
return nil, fmt.Errorf("location given but empty")
|
||||||
@ -144,6 +146,7 @@ func ReadConfigData(location string) ([]byte, error) {
|
|||||||
return ReadConfigDataFromLocation(location)
|
return ReadConfigDataFromLocation(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: replace with resource.Builder
|
||||||
func ReadConfigDataFromLocation(location string) ([]byte, error) {
|
func ReadConfigDataFromLocation(location string) ([]byte, error) {
|
||||||
// we look for http:// or https:// to determine if valid URL, otherwise do normal file IO
|
// we look for http:// or https:// to determine if valid URL, otherwise do normal file IO
|
||||||
if strings.Index(location, "http://") == 0 || strings.Index(location, "https://") == 0 {
|
if strings.Index(location, "http://") == 0 || strings.Index(location, "https://") == 0 {
|
||||||
|
563
pkg/kubectl/resource/builder.go
Normal file
563
pkg/kubectl/resource/builder.go
Normal file
@ -0,0 +1,563 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
resources []string
|
||||||
|
|
||||||
|
namespace string
|
||||||
|
name string
|
||||||
|
|
||||||
|
defaultNamespace bool
|
||||||
|
requireNamespace bool
|
||||||
|
|
||||||
|
flatten bool
|
||||||
|
latest bool
|
||||||
|
|
||||||
|
singleResourceType bool
|
||||||
|
continueOnError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuilder creates a builder that operates on generic objects.
|
||||||
|
func NewBuilder(mapper meta.RESTMapper, typer runtime.ObjectTyper, clientMapper ClientMapper) *Builder {
|
||||||
|
return &Builder{
|
||||||
|
mapper: &Mapper{typer, mapper, clientMapper},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename is parameters passed via a filename argument which may be URLs, the "-" argument indicating
|
||||||
|
// STDIN, or paths to files or directories. If ContinueOnError() is set prior to this method being called,
|
||||||
|
// objects on the path that are unrecognized will be ignored (but logged at V(2)).
|
||||||
|
func (b *Builder) FilenameParam(paths ...string) *Builder {
|
||||||
|
for _, s := range paths {
|
||||||
|
switch {
|
||||||
|
case s == "-":
|
||||||
|
b.Stdin()
|
||||||
|
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(url)
|
||||||
|
default:
|
||||||
|
b.Path(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL accepts a number of URLs directly.
|
||||||
|
func (b *Builder) URL(urls ...*url.URL) *Builder {
|
||||||
|
for _, u := range urls {
|
||||||
|
b.paths = append(b.paths, &URLVisitor{
|
||||||
|
Mapper: b.mapper,
|
||||||
|
URL: u,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stdin will read objects from the standard input. If ContinueOnError() is set
|
||||||
|
// prior to this method being called, objects in the stream that are unrecognized
|
||||||
|
// will be ignored (but logged at V(2)).
|
||||||
|
func (b *Builder) Stdin() *Builder {
|
||||||
|
return b.Stream(os.Stdin, "STDIN")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream will read objects from the provided reader, and if an error occurs will
|
||||||
|
// include the name string in the error message. If ContinueOnError() is set
|
||||||
|
// prior to this method being called, objects in the stream that are unrecognized
|
||||||
|
// will be ignored (but logged at V(2)).
|
||||||
|
func (b *Builder) Stream(r io.Reader, name string) *Builder {
|
||||||
|
b.stream = true
|
||||||
|
b.paths = append(b.paths, NewStreamVisitor(r, b.mapper, name, b.continueOnError))
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path is a set of filesystem paths that may be files containing one or more
|
||||||
|
// resources. If ContinueOnError() is set prior to this method being called,
|
||||||
|
// objects on the path that are unrecognized will be ignored (but logged at V(2)).
|
||||||
|
func (b *Builder) Path(paths ...string) *Builder {
|
||||||
|
for _, p := range paths {
|
||||||
|
i, 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
|
||||||
|
}
|
||||||
|
var visitor Visitor
|
||||||
|
if i.IsDir() {
|
||||||
|
b.dir = true
|
||||||
|
visitor = &DirectoryVisitor{
|
||||||
|
Mapper: b.mapper,
|
||||||
|
Path: p,
|
||||||
|
Extensions: []string{".json", ".yaml"},
|
||||||
|
Recursive: false,
|
||||||
|
IgnoreErrors: b.continueOnError,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
visitor = &PathVisitor{
|
||||||
|
Mapper: b.mapper,
|
||||||
|
Path: p,
|
||||||
|
IgnoreErrors: b.continueOnError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.paths = append(b.paths, visitor)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceTypes is a list of types of resources to operate on, when listing objects on
|
||||||
|
// the server or retrieving objects that match a selector.
|
||||||
|
func (b *Builder) ResourceTypes(types ...string) *Builder {
|
||||||
|
b.resources = append(b.resources, types...)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectorParam defines a selector that should be applied to the object types to load.
|
||||||
|
// This will not affect files loaded from disk or URL. If the parameter is empty it is
|
||||||
|
// a no-op - to select all resources invoke `b.Selector(labels.Everything)`.
|
||||||
|
func (b *Builder) SelectorParam(s string) *Builder {
|
||||||
|
selector, err := labels.ParseSelector(s)
|
||||||
|
if err != nil {
|
||||||
|
b.errs = append(b.errs, fmt.Errorf("the provided selector %q is not valid: %v", s, err))
|
||||||
|
}
|
||||||
|
if selector.Empty() {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return b.Selector(selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selector accepts a selector directly, and if non nil will trigger a list action.
|
||||||
|
func (b *Builder) Selector(selector labels.Selector) *Builder {
|
||||||
|
b.selector = selector
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// The namespace that these resources should be assumed to under - used by DefaultNamespace()
|
||||||
|
// and RequireNamespace()
|
||||||
|
func (b *Builder) NamespaceParam(namespace string) *Builder {
|
||||||
|
b.namespace = namespace
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultNamespace instructs the builder to set the namespace value for any object found
|
||||||
|
// to NamespaceParam() if empty.
|
||||||
|
func (b *Builder) DefaultNamespace() *Builder {
|
||||||
|
b.defaultNamespace = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireNamespace instructs the builder to set the namespace value for any object found
|
||||||
|
// to NamespaceParam() if empty, and if the value on the resource does not match
|
||||||
|
// NamespaceParam() an error will be returned.
|
||||||
|
func (b *Builder) RequireNamespace() *Builder {
|
||||||
|
b.requireNamespace = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceTypeOrNameArgs indicates that the builder should accept one or two arguments
|
||||||
|
// of the form `(<type1>[,<type2>,...]|<type> <name>)`. When one argument is received, the types
|
||||||
|
// provided will be retrieved from the server (and be comma delimited). When two arguments are
|
||||||
|
// received, they must be a single type and name. If more than two arguments are provided an
|
||||||
|
// error is set.
|
||||||
|
func (b *Builder) ResourceTypeOrNameArgs(args ...string) *Builder {
|
||||||
|
switch len(args) {
|
||||||
|
case 2:
|
||||||
|
b.name = args[1]
|
||||||
|
b.ResourceTypes(SplitResourceArgument(args[0])...)
|
||||||
|
case 1:
|
||||||
|
b.ResourceTypes(SplitResourceArgument(args[0])...)
|
||||||
|
if b.selector == nil {
|
||||||
|
b.selector = labels.Everything()
|
||||||
|
}
|
||||||
|
case 0:
|
||||||
|
default:
|
||||||
|
b.errs = append(b.errs, fmt.Errorf("when passing arguments, must be resource or resource and name"))
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
switch len(args) {
|
||||||
|
case 2:
|
||||||
|
b.name = args[1]
|
||||||
|
b.ResourceTypes(SplitResourceArgument(args[0])...)
|
||||||
|
case 0:
|
||||||
|
default:
|
||||||
|
b.errs = append(b.errs, fmt.Errorf("when passing arguments, must be resource and name"))
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten will convert any objects with a field named "Items" that is an array of runtime.Object
|
||||||
|
// compatible types into individual entries and give them their own items. The original object
|
||||||
|
// is not passed to any visitors.
|
||||||
|
func (b *Builder) Flatten() *Builder {
|
||||||
|
b.flatten = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latest will fetch the latest copy of any objects loaded from URLs or files from the server.
|
||||||
|
func (b *Builder) Latest() *Builder {
|
||||||
|
b.latest = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContinueOnError will attempt to load and visit as many objects as possible, even if some visits
|
||||||
|
// return errors or some objects cannot be loaded. The default behavior is to terminate after
|
||||||
|
// the first error is returned from a VisitorFunc.
|
||||||
|
func (b *Builder) ContinueOnError() *Builder {
|
||||||
|
b.continueOnError = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) SingleResourceType() *Builder {
|
||||||
|
b.singleResourceType = true
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
version, kind, err := b.mapper.VersionAndKindForResource(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mapping, err := b.mapper.RESTMapping(kind, version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mappings = append(mappings, mapping)
|
||||||
|
}
|
||||||
|
return mappings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) visitorResult() *Result {
|
||||||
|
if len(b.errs) > 0 {
|
||||||
|
return &Result{err: errors.NewAggregate(b.errs)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// visit selectors
|
||||||
|
if b.selector != nil {
|
||||||
|
if len(b.name) != 0 {
|
||||||
|
return &Result{err: fmt.Errorf("name cannot be provided when a selector is specified")}
|
||||||
|
}
|
||||||
|
if len(b.resources) == 0 {
|
||||||
|
return &Result{err: fmt.Errorf("at least one resource must be specified to use a selector")}
|
||||||
|
}
|
||||||
|
// empty selector has different error message for paths being provided
|
||||||
|
if len(b.paths) != 0 {
|
||||||
|
if b.selector.Empty() {
|
||||||
|
return &Result{err: fmt.Errorf("when paths, URLs, or stdin is provided as input, you may not specify a resource by arguments as well")}
|
||||||
|
} else {
|
||||||
|
return &Result{err: fmt.Errorf("a selector may not be specified when path, URL, or stdin is provided as input")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mappings, err := b.resourceMappings()
|
||||||
|
if err != nil {
|
||||||
|
return &Result{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
visitors := []Visitor{}
|
||||||
|
for _, mapping := range mappings {
|
||||||
|
client, err := b.mapper.ClientForMapping(mapping)
|
||||||
|
if err != nil {
|
||||||
|
return &Result{err: err}
|
||||||
|
}
|
||||||
|
visitors = append(visitors, NewSelector(client, mapping, b.namespace, b.selector))
|
||||||
|
}
|
||||||
|
if b.continueOnError {
|
||||||
|
return &Result{visitor: EagerVisitorList(visitors), sources: visitors}
|
||||||
|
}
|
||||||
|
return &Result{visitor: VisitorList(visitors), sources: visitors}
|
||||||
|
}
|
||||||
|
|
||||||
|
// visit single item specified by name
|
||||||
|
if len(b.name) != 0 {
|
||||||
|
if len(b.paths) != 0 {
|
||||||
|
return &Result{singular: true, 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: true, err: fmt.Errorf("you must provide a resource and a resource name together")}
|
||||||
|
}
|
||||||
|
if len(b.resources) > 1 {
|
||||||
|
return &Result{singular: true, err: fmt.Errorf("you must specify only one resource")}
|
||||||
|
}
|
||||||
|
if len(b.namespace) == 0 {
|
||||||
|
return &Result{singular: true, err: fmt.Errorf("namespace may not be empty when retrieving a resource by name")}
|
||||||
|
}
|
||||||
|
mappings, err := b.resourceMappings()
|
||||||
|
if err != nil {
|
||||||
|
return &Result{singular: true, err: err}
|
||||||
|
}
|
||||||
|
client, err := b.mapper.ClientForMapping(mappings[0])
|
||||||
|
if err != nil {
|
||||||
|
return &Result{singular: true, err: err}
|
||||||
|
}
|
||||||
|
info := NewInfo(client, mappings[0], b.namespace, b.name)
|
||||||
|
if err := info.Get(); err != nil {
|
||||||
|
return &Result{singular: true, err: err}
|
||||||
|
}
|
||||||
|
return &Result{singular: true, visitor: info, sources: []Visitor{info}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// visit items specified by paths
|
||||||
|
if len(b.paths) != 0 {
|
||||||
|
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")}
|
||||||
|
}
|
||||||
|
|
||||||
|
var visitors Visitor
|
||||||
|
if b.continueOnError {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
visitors = NewDecoratedVisitor(visitors, RetrieveLatest)
|
||||||
|
}
|
||||||
|
return &Result{singular: singular, visitor: visitors, sources: b.paths}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Result{err: fmt.Errorf("you must provide one or more resources by argument or filename")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do returns a Result object with a Visitor for the resources identified by the Builder.
|
||||||
|
// The visitor will respect the error behavior specified by ContinueOnError. Note that stream
|
||||||
|
// inputs are consumed by the first execution - use Infos() or Object() on the Result to capture a list
|
||||||
|
// for further iteration.
|
||||||
|
func (b *Builder) Do() *Result {
|
||||||
|
r := b.visitorResult()
|
||||||
|
if r.err != nil {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
r.visitor = NewDecoratedVisitor(r.visitor, helpers...)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result contains helper methods for dealing with the outcome of a Builder.
|
||||||
|
type Result struct {
|
||||||
|
err error
|
||||||
|
visitor Visitor
|
||||||
|
|
||||||
|
sources []Visitor
|
||||||
|
singular bool
|
||||||
|
|
||||||
|
// populated by a call to Infos
|
||||||
|
info []*Info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns one or more errors (via a util.ErrorList) that occurred prior
|
||||||
|
// to visiting the elements in the visitor. To see all errors including those
|
||||||
|
// that occur during visitation, invoke Infos().
|
||||||
|
func (r *Result) Err() error {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit implements the Visitor interface on the items described in the Builder.
|
||||||
|
// Note that some visitor sources are not traversable more than once, or may
|
||||||
|
// return different results. If you wish to operate on the same set of resources
|
||||||
|
// multiple times, use the Infos() method.
|
||||||
|
func (r *Result) Visit(fn VisitorFunc) error {
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
return r.visitor.Visit(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntoSingular sets the provided boolean pointer to true if the Builder input
|
||||||
|
// reflected a single item, or multiple.
|
||||||
|
func (r *Result) IntoSingular(b *bool) *Result {
|
||||||
|
*b = r.singular
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infos returns an array of all of the resource infos retrieved via traversal.
|
||||||
|
// Will attempt to traverse the entire set of visitors only once, and will return
|
||||||
|
// a cached list on subsequent calls.
|
||||||
|
func (r *Result) Infos() ([]*Info, error) {
|
||||||
|
if r.err != nil {
|
||||||
|
return nil, r.err
|
||||||
|
}
|
||||||
|
if r.info != nil {
|
||||||
|
return r.info, nil
|
||||||
|
}
|
||||||
|
infos := []*Info{}
|
||||||
|
err := r.visitor.Visit(func(info *Info) error {
|
||||||
|
infos = append(infos, info)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
r.info, r.err = infos, err
|
||||||
|
return infos, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object returns a single object representing the output of a single visit to all
|
||||||
|
// found resources. If the Builder was a singular context (expected to return a
|
||||||
|
// single resource by user input) and only a single resource was found, the resource
|
||||||
|
// will be returned as is. Otherwise, the returned resources will be part of an
|
||||||
|
// api.List. The ResourceVersion of the api.List will be set only if it is identical
|
||||||
|
// across all infos returned.
|
||||||
|
func (r *Result) Object() (runtime.Object, error) {
|
||||||
|
infos, err := r.Infos()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
versions := util.StringSet{}
|
||||||
|
objects := []runtime.Object{}
|
||||||
|
for _, info := range infos {
|
||||||
|
if info.Object != nil {
|
||||||
|
objects = append(objects, info.Object)
|
||||||
|
versions.Insert(info.ResourceVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(objects) == 1 {
|
||||||
|
if r.singular {
|
||||||
|
return objects[0], nil
|
||||||
|
}
|
||||||
|
// if the item is a list already, don't create another list
|
||||||
|
if _, err := runtime.GetItemsPtr(objects[0]); err == nil {
|
||||||
|
return objects[0], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
version := ""
|
||||||
|
if len(versions) == 1 {
|
||||||
|
version = versions.List()[0]
|
||||||
|
}
|
||||||
|
return &api.List{
|
||||||
|
ListMeta: api.ListMeta{
|
||||||
|
ResourceVersion: version,
|
||||||
|
},
|
||||||
|
Items: objects,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceMapping returns a single meta.RESTMapping representing the
|
||||||
|
// resources located by the builder, or an error if more than one
|
||||||
|
// mapping was found.
|
||||||
|
func (r *Result) ResourceMapping() (*meta.RESTMapping, error) {
|
||||||
|
if r.err != nil {
|
||||||
|
return nil, r.err
|
||||||
|
}
|
||||||
|
mappings := map[string]*meta.RESTMapping{}
|
||||||
|
for i := range r.sources {
|
||||||
|
m, ok := r.sources[i].(ResourceMapping)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("a resource mapping could not be loaded from %v", reflect.TypeOf(r.sources[i]))
|
||||||
|
}
|
||||||
|
mapping := m.ResourceMapping()
|
||||||
|
mappings[mapping.Resource] = mapping
|
||||||
|
}
|
||||||
|
if len(mappings) != 1 {
|
||||||
|
return nil, fmt.Errorf("expected only a single resource type")
|
||||||
|
}
|
||||||
|
for _, mapping := range mappings {
|
||||||
|
return mapping, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch retrieves changes that occur on the server to the specified resource.
|
||||||
|
// It currently supports watching a single source - if the resource source
|
||||||
|
// (selectors or pure types) can be watched, they will be, otherwise the list
|
||||||
|
// will be visited (equivalent to the Infos() call) and if there is a single
|
||||||
|
// resource present, it will be watched, otherwise an error will be returned.
|
||||||
|
func (r *Result) Watch(resourceVersion string) (watch.Interface, error) {
|
||||||
|
if r.err != nil {
|
||||||
|
return nil, r.err
|
||||||
|
}
|
||||||
|
if len(r.sources) != 1 {
|
||||||
|
return nil, fmt.Errorf("you may only watch a single resource or type of resource at a time")
|
||||||
|
}
|
||||||
|
w, ok := r.sources[0].(Watchable)
|
||||||
|
if !ok {
|
||||||
|
info, err := r.Infos()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(info) != 1 {
|
||||||
|
return nil, fmt.Errorf("watch is only supported on a single resource - %d resources were found", len(info))
|
||||||
|
}
|
||||||
|
return info[0].Watch(resourceVersion)
|
||||||
|
}
|
||||||
|
return w.Watch(resourceVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitResourceArgument(arg string) []string {
|
||||||
|
set := util.NewStringSet()
|
||||||
|
set.Insert(strings.Split(arg, ",")...)
|
||||||
|
return set.List()
|
||||||
|
}
|
619
pkg/kubectl/resource/builder_test.go
Normal file
619
pkg/kubectl/resource/builder_test.go
Normal file
@ -0,0 +1,619 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||||
|
watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
func stringBody(body string) io.ReadCloser {
|
||||||
|
return ioutil.NopCloser(bytes.NewReader([]byte(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchBody(events ...watch.Event) string {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
enc := watchjson.NewEncoder(buf, latest.Codec)
|
||||||
|
for _, e := range events {
|
||||||
|
enc.Encode(&e)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fakeClient() ClientMapper {
|
||||||
|
return ClientMapperFunc(func(*meta.RESTMapping) (RESTClient, error) {
|
||||||
|
return &client.FakeRESTClient{}, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fakeClientWith(t *testing.T, data map[string]string) ClientMapper {
|
||||||
|
return ClientMapperFunc(func(*meta.RESTMapping) (RESTClient, error) {
|
||||||
|
return &client.FakeRESTClient{
|
||||||
|
Codec: latest.Codec,
|
||||||
|
Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) {
|
||||||
|
p := req.URL.Path
|
||||||
|
q := req.URL.RawQuery
|
||||||
|
if len(q) != 0 {
|
||||||
|
p = p + "?" + q
|
||||||
|
}
|
||||||
|
body, ok := data[p]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected request: %s (%s)\n%#v", p, req.URL, req)
|
||||||
|
}
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: stringBody(body),
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testData() (*api.PodList, *api.ServiceList) {
|
||||||
|
pods := &api.PodList{
|
||||||
|
ListMeta: api.ListMeta{
|
||||||
|
ResourceVersion: "15",
|
||||||
|
},
|
||||||
|
Items: []api.Pod{
|
||||||
|
{
|
||||||
|
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: api.ObjectMeta{Name: "bar", Namespace: "test", ResourceVersion: "11"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svc := &api.ServiceList{
|
||||||
|
ListMeta: api.ListMeta{
|
||||||
|
ResourceVersion: "16",
|
||||||
|
},
|
||||||
|
Items: []api.Service{
|
||||||
|
{
|
||||||
|
ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return pods, svc
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamTestData() (io.Reader, *api.PodList, *api.ServiceList) {
|
||||||
|
pods, svc := testData()
|
||||||
|
r, w := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
defer w.Close()
|
||||||
|
w.Write([]byte(runtime.EncodeOrDie(latest.Codec, pods)))
|
||||||
|
w.Write([]byte(runtime.EncodeOrDie(latest.Codec, svc)))
|
||||||
|
}()
|
||||||
|
return r, pods, svc
|
||||||
|
}
|
||||||
|
|
||||||
|
type testVisitor struct {
|
||||||
|
InjectErr error
|
||||||
|
Infos []*Info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *testVisitor) Handle(info *Info) error {
|
||||||
|
v.Infos = append(v.Infos, info)
|
||||||
|
return v.InjectErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *testVisitor) Objects() []runtime.Object {
|
||||||
|
objects := []runtime.Object{}
|
||||||
|
for i := range v.Infos {
|
||||||
|
objects = append(objects, v.Infos[i].Object)
|
||||||
|
}
|
||||||
|
return objects
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathBuilder(t *testing.T) {
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
FilenameParam("../../../examples/guestbook/redis-master.json")
|
||||||
|
|
||||||
|
test := &testVisitor{}
|
||||||
|
singular := false
|
||||||
|
|
||||||
|
err := b.Do().IntoSingular(&singular).Visit(test.Handle)
|
||||||
|
if err != nil || !singular || len(test.Infos) != 1 {
|
||||||
|
t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos)
|
||||||
|
}
|
||||||
|
|
||||||
|
info := test.Infos[0]
|
||||||
|
if info.Name != "redis-master" || info.Namespace != "" || info.Object == nil {
|
||||||
|
t.Errorf("unexpected info: %#v", info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathBuilderWithMultiple(t *testing.T) {
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
FilenameParam("../../../examples/guestbook/redis-master.json").
|
||||||
|
FilenameParam("../../../examples/guestbook/redis-master.json").
|
||||||
|
NamespaceParam("test").DefaultNamespace()
|
||||||
|
|
||||||
|
test := &testVisitor{}
|
||||||
|
singular := false
|
||||||
|
|
||||||
|
err := b.Do().IntoSingular(&singular).Visit(test.Handle)
|
||||||
|
if err != nil || singular || len(test.Infos) != 2 {
|
||||||
|
t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos)
|
||||||
|
}
|
||||||
|
|
||||||
|
info := test.Infos[1]
|
||||||
|
if info.Name != "redis-master" || info.Namespace != "test" || info.Object == nil {
|
||||||
|
t.Errorf("unexpected info: %#v", info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirectoryBuilder(t *testing.T) {
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
FilenameParam("../../../examples/guestbook").
|
||||||
|
NamespaceParam("test").DefaultNamespace()
|
||||||
|
|
||||||
|
test := &testVisitor{}
|
||||||
|
singular := false
|
||||||
|
|
||||||
|
err := b.Do().IntoSingular(&singular).Visit(test.Handle)
|
||||||
|
if err != nil || singular || len(test.Infos) < 4 {
|
||||||
|
t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos)
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, info := range test.Infos {
|
||||||
|
if info.Name == "redis-master" && info.Namespace == "test" && info.Object != nil {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("unexpected responses: %#v", test.Infos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURLBuilder(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(runtime.EncodeOrDie(latest.Codec, &api.Pod{ObjectMeta: api.ObjectMeta{Namespace: "foo", Name: "test"}})))
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
FilenameParam(s.URL).
|
||||||
|
NamespaceParam("test")
|
||||||
|
|
||||||
|
test := &testVisitor{}
|
||||||
|
singular := false
|
||||||
|
|
||||||
|
err := b.Do().IntoSingular(&singular).Visit(test.Handle)
|
||||||
|
if err != nil || !singular || len(test.Infos) != 1 {
|
||||||
|
t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos)
|
||||||
|
}
|
||||||
|
info := test.Infos[0]
|
||||||
|
if info.Name != "test" || info.Namespace != "foo" || info.Object == nil {
|
||||||
|
t.Errorf("unexpected info: %#v", info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURLBuilderRequireNamespace(t *testing.T) {
|
||||||
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(runtime.EncodeOrDie(latest.Codec, &api.Pod{ObjectMeta: api.ObjectMeta{Namespace: "foo", Name: "test"}})))
|
||||||
|
}))
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
FilenameParam(s.URL).
|
||||||
|
NamespaceParam("test").RequireNamespace()
|
||||||
|
|
||||||
|
test := &testVisitor{}
|
||||||
|
singular := false
|
||||||
|
|
||||||
|
err := b.Do().IntoSingular(&singular).Visit(test.Handle)
|
||||||
|
if err == nil || !singular || len(test.Infos) != 0 {
|
||||||
|
t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceByName(t *testing.T) {
|
||||||
|
pods, _ := testData()
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{
|
||||||
|
"/ns/test/pods/foo": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]),
|
||||||
|
})).
|
||||||
|
NamespaceParam("test")
|
||||||
|
|
||||||
|
test := &testVisitor{}
|
||||||
|
singular := false
|
||||||
|
|
||||||
|
if b.Do().Err() == nil {
|
||||||
|
t.Errorf("unexpected non-error")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResourceTypeOrNameArgs("pods", "foo")
|
||||||
|
|
||||||
|
err := b.Do().IntoSingular(&singular).Visit(test.Handle)
|
||||||
|
if err != nil || !singular || len(test.Infos) != 1 {
|
||||||
|
t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(&pods.Items[0], test.Objects()[0]) {
|
||||||
|
t.Errorf("unexpected object: %#v", test.Objects())
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping, err := b.Do().ResourceMapping()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if mapping.Resource != "pods" {
|
||||||
|
t.Errorf("unexpected resource mapping: %#v", mapping)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceByNameAndEmptySelector(t *testing.T) {
|
||||||
|
pods, _ := testData()
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{
|
||||||
|
"/ns/test/pods/foo": runtime.EncodeOrDie(latest.Codec, &pods.Items[0]),
|
||||||
|
})).
|
||||||
|
NamespaceParam("test").
|
||||||
|
SelectorParam("").
|
||||||
|
ResourceTypeOrNameArgs("pods", "foo")
|
||||||
|
|
||||||
|
singular := false
|
||||||
|
infos, err := b.Do().IntoSingular(&singular).Infos()
|
||||||
|
if err != nil || !singular || len(infos) != 1 {
|
||||||
|
t.Fatalf("unexpected response: %v %f %#v", err, singular, infos)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(&pods.Items[0], infos[0].Object) {
|
||||||
|
t.Errorf("unexpected object: %#v", infos[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping, err := b.Do().ResourceMapping()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if mapping.Resource != "pods" {
|
||||||
|
t.Errorf("unexpected resource mapping: %#v", mapping)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelector(t *testing.T) {
|
||||||
|
pods, svc := testData()
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{
|
||||||
|
"/ns/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods),
|
||||||
|
"/ns/test/services?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, svc),
|
||||||
|
})).
|
||||||
|
SelectorParam("a=b").
|
||||||
|
NamespaceParam("test").
|
||||||
|
Flatten()
|
||||||
|
|
||||||
|
test := &testVisitor{}
|
||||||
|
singular := false
|
||||||
|
|
||||||
|
if b.Do().Err() == nil {
|
||||||
|
t.Errorf("unexpected non-error")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResourceTypeOrNameArgs("pods,service")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual([]runtime.Object{&pods.Items[0], &pods.Items[1], &svc.Items[0]}, test.Objects()) {
|
||||||
|
t.Errorf("unexpected visited objects: %#v", test.Objects())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := b.Do().ResourceMapping(); err == nil {
|
||||||
|
t.Errorf("unexpected non-error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectorRequiresKnownTypes(t *testing.T) {
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
SelectorParam("a=b").
|
||||||
|
NamespaceParam("test").
|
||||||
|
ResourceTypes("unknown")
|
||||||
|
|
||||||
|
if b.Do().Err() == nil {
|
||||||
|
t.Errorf("unexpected non-error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSingleResourceType(t *testing.T) {
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
SelectorParam("a=b").
|
||||||
|
SingleResourceType().
|
||||||
|
ResourceTypeOrNameArgs("pods,services")
|
||||||
|
|
||||||
|
if b.Do().Err() == nil {
|
||||||
|
t.Errorf("unexpected non-error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStream(t *testing.T) {
|
||||||
|
r, pods, rc := streamTestData()
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
NamespaceParam("test").Stream(r, "STDIN").Flatten()
|
||||||
|
|
||||||
|
test := &testVisitor{}
|
||||||
|
singular := false
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual([]runtime.Object{&pods.Items[0], &pods.Items[1], &rc.Items[0]}, test.Objects()) {
|
||||||
|
t.Errorf("unexpected visited objects: %#v", test.Objects())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultipleObject(t *testing.T) {
|
||||||
|
r, pods, svc := streamTestData()
|
||||||
|
obj, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
NamespaceParam("test").Stream(r, "STDIN").Flatten().
|
||||||
|
Do().Object()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := &api.List{
|
||||||
|
Items: []runtime.Object{
|
||||||
|
&pods.Items[0],
|
||||||
|
&pods.Items[1],
|
||||||
|
&svc.Items[0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(expected, obj) {
|
||||||
|
t.Errorf("unexpected visited objects: %#v", obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSingularObject(t *testing.T) {
|
||||||
|
obj, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
NamespaceParam("test").DefaultNamespace().
|
||||||
|
FilenameParam("../../../examples/guestbook/redis-master.json").
|
||||||
|
Flatten().
|
||||||
|
Do().Object()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pod, ok := obj.(*api.Pod)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected object: %#v", obj)
|
||||||
|
}
|
||||||
|
if pod.Name != "redis-master" || pod.Namespace != "test" {
|
||||||
|
t.Errorf("unexpected pod: %#v", pod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListObject(t *testing.T) {
|
||||||
|
pods, _ := testData()
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{
|
||||||
|
"/ns/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods),
|
||||||
|
})).
|
||||||
|
SelectorParam("a=b").
|
||||||
|
NamespaceParam("test").
|
||||||
|
ResourceTypeOrNameArgs("pods").
|
||||||
|
Flatten()
|
||||||
|
|
||||||
|
obj, err := b.Do().Object()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
list, ok := obj.(*api.List)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected object: %#v", obj)
|
||||||
|
}
|
||||||
|
if list.ResourceVersion != pods.ResourceVersion || len(list.Items) != 2 {
|
||||||
|
t.Errorf("unexpected list: %#v", list)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping, err := b.Do().ResourceMapping()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if mapping.Resource != "pods" {
|
||||||
|
t.Errorf("unexpected resource mapping: %#v", mapping)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListObjectWithDifferentVersions(t *testing.T) {
|
||||||
|
pods, svc := testData()
|
||||||
|
obj, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{
|
||||||
|
"/ns/test/pods?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, pods),
|
||||||
|
"/ns/test/services?labels=a%3Db": runtime.EncodeOrDie(latest.Codec, svc),
|
||||||
|
})).
|
||||||
|
SelectorParam("a=b").
|
||||||
|
NamespaceParam("test").
|
||||||
|
ResourceTypeOrNameArgs("pods,services").
|
||||||
|
Flatten().
|
||||||
|
Do().Object()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
list, ok := obj.(*api.List)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected object: %#v", obj)
|
||||||
|
}
|
||||||
|
// resource version differs between type lists, so it's not possible to get a single version.
|
||||||
|
if list.ResourceVersion != "" || len(list.Items) != 3 {
|
||||||
|
t.Errorf("unexpected list: %#v", list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatch(t *testing.T) {
|
||||||
|
pods, _ := testData()
|
||||||
|
w, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{
|
||||||
|
"/watch/ns/test/pods/redis-master?resourceVersion=10": watchBody(watch.Event{
|
||||||
|
Type: watch.Added,
|
||||||
|
Object: &pods.Items[0],
|
||||||
|
}),
|
||||||
|
})).
|
||||||
|
NamespaceParam("test").DefaultNamespace().
|
||||||
|
FilenameParam("../../../examples/guestbook/redis-master.json").Flatten().
|
||||||
|
Do().Watch("10")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer w.Stop()
|
||||||
|
ch := w.ResultChan()
|
||||||
|
select {
|
||||||
|
case obj := <-ch:
|
||||||
|
if obj.Type != watch.Added {
|
||||||
|
t.Fatalf("unexpected watch event", obj)
|
||||||
|
}
|
||||||
|
pod, ok := obj.Object.(*api.Pod)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected object: %#v", obj)
|
||||||
|
}
|
||||||
|
if pod.Name != "foo" || pod.ResourceVersion != "10" {
|
||||||
|
t.Errorf("unexpected pod: %#v", pod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatchMultipleError(t *testing.T) {
|
||||||
|
_, err := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
NamespaceParam("test").DefaultNamespace().
|
||||||
|
FilenameParam("../../../examples/guestbook/redis-master.json").Flatten().
|
||||||
|
FilenameParam("../../../examples/guestbook/redis-master.json").Flatten().
|
||||||
|
Do().Watch("")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("unexpected non-error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLatest(t *testing.T) {
|
||||||
|
r, _, _ := streamTestData()
|
||||||
|
newPod := &api.Pod{
|
||||||
|
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "13"},
|
||||||
|
}
|
||||||
|
newPod2 := &api.Pod{
|
||||||
|
ObjectMeta: api.ObjectMeta{Name: "bar", Namespace: "test", ResourceVersion: "14"},
|
||||||
|
}
|
||||||
|
newSvc := &api.Service{
|
||||||
|
ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "15"},
|
||||||
|
}
|
||||||
|
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClientWith(t, map[string]string{
|
||||||
|
"/ns/test/pods/foo": runtime.EncodeOrDie(latest.Codec, newPod),
|
||||||
|
"/ns/test/pods/bar": runtime.EncodeOrDie(latest.Codec, newPod2),
|
||||||
|
"/ns/test/services/baz": runtime.EncodeOrDie(latest.Codec, newSvc),
|
||||||
|
})).
|
||||||
|
NamespaceParam("other").Stream(r, "STDIN").Flatten().Latest()
|
||||||
|
|
||||||
|
test := &testVisitor{}
|
||||||
|
singular := false
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual([]runtime.Object{newPod, newPod2, newSvc}, test.Objects()) {
|
||||||
|
t.Errorf("unexpected visited objects: %#v", test.Objects())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIgnoreStreamErrors(t *testing.T) {
|
||||||
|
pods, svc := testData()
|
||||||
|
|
||||||
|
r, w := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
defer w.Close()
|
||||||
|
w.Write([]byte(`{}`))
|
||||||
|
w.Write([]byte(runtime.EncodeOrDie(latest.Codec, &pods.Items[0])))
|
||||||
|
}()
|
||||||
|
|
||||||
|
r2, w2 := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
defer w2.Close()
|
||||||
|
w2.Write([]byte(`{}`))
|
||||||
|
w2.Write([]byte(runtime.EncodeOrDie(latest.Codec, &svc.Items[0])))
|
||||||
|
}()
|
||||||
|
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
ContinueOnError(). // TODO: order seems bad, but allows clients to determine what they want...
|
||||||
|
Stream(r, "1").Stream(r2, "2")
|
||||||
|
|
||||||
|
test := &testVisitor{}
|
||||||
|
singular := false
|
||||||
|
|
||||||
|
err := b.Do().IntoSingular(&singular).Visit(test.Handle)
|
||||||
|
if err != nil || singular || len(test.Infos) != 2 {
|
||||||
|
t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual([]runtime.Object{&pods.Items[0], &svc.Items[0]}, test.Objects()) {
|
||||||
|
t.Errorf("unexpected visited objects: %#v", test.Objects())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReceiveMultipleErrors(t *testing.T) {
|
||||||
|
pods, svc := testData()
|
||||||
|
|
||||||
|
r, w := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
defer w.Close()
|
||||||
|
w.Write([]byte(`{}`))
|
||||||
|
w.Write([]byte(runtime.EncodeOrDie(latest.Codec, &pods.Items[0])))
|
||||||
|
}()
|
||||||
|
|
||||||
|
r2, w2 := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
defer w2.Close()
|
||||||
|
w2.Write([]byte(`{}`))
|
||||||
|
w2.Write([]byte(runtime.EncodeOrDie(latest.Codec, &svc.Items[0])))
|
||||||
|
}()
|
||||||
|
|
||||||
|
b := NewBuilder(latest.RESTMapper, api.Scheme, fakeClient()).
|
||||||
|
Stream(r, "1").Stream(r2, "2").
|
||||||
|
ContinueOnError()
|
||||||
|
|
||||||
|
test := &testVisitor{}
|
||||||
|
singular := false
|
||||||
|
|
||||||
|
err := b.Do().IntoSingular(&singular).Visit(test.Handle)
|
||||||
|
if err == nil || singular || len(test.Infos) != 0 {
|
||||||
|
t.Fatalf("unexpected response: %v %f %#v", err, singular, test.Infos)
|
||||||
|
}
|
||||||
|
|
||||||
|
errs, ok := err.(errors.Aggregate)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected error: %v", reflect.TypeOf(err))
|
||||||
|
}
|
||||||
|
if len(errs.Errors()) != 2 {
|
||||||
|
t.Errorf("unexpected errors", errs)
|
||||||
|
}
|
||||||
|
}
|
24
pkg/kubectl/resource/doc.go
Normal file
24
pkg/kubectl/resource/doc.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package resource assists clients in dealing with RESTful objects that match the
|
||||||
|
// Kubernetes API conventions. The Helper object provides simple CRUD operations
|
||||||
|
// on resources. The Visitor interface makes it easy to deal with multiple resources
|
||||||
|
// in bulk for retrieval and operation. The Builder object simplifies converting
|
||||||
|
// standard command line arguments and parameters into a Visitor that can iterate
|
||||||
|
// over all of the identified resources, whether on the server or on the local
|
||||||
|
// filesystem.
|
||||||
|
package resource
|
172
pkg/kubectl/resource/helper.go
Normal file
172
pkg/kubectl/resource/helper.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper provides methods for retrieving or mutating a RESTful
|
||||||
|
// resource.
|
||||||
|
type Helper struct {
|
||||||
|
// The name of this resource as the server would recognize it
|
||||||
|
Resource string
|
||||||
|
// A RESTClient capable of mutating this resource
|
||||||
|
RESTClient RESTClient
|
||||||
|
// A codec for decoding and encoding objects of this resource type.
|
||||||
|
Codec runtime.Codec
|
||||||
|
// An interface for reading or writing the resource version of this
|
||||||
|
// type.
|
||||||
|
Versioner runtime.ResourceVersioner
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHelper creates a Helper from a ResourceMapping
|
||||||
|
func NewHelper(client RESTClient, mapping *meta.RESTMapping) *Helper {
|
||||||
|
return &Helper{
|
||||||
|
RESTClient: client,
|
||||||
|
Resource: mapping.Resource,
|
||||||
|
Codec: mapping.Codec,
|
||||||
|
Versioner: mapping.MetadataAccessor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Helper) Get(namespace, name string) (runtime.Object, error) {
|
||||||
|
return m.RESTClient.Get().
|
||||||
|
Namespace(namespace).
|
||||||
|
Resource(m.Resource).
|
||||||
|
Name(name).
|
||||||
|
Do().
|
||||||
|
Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Helper) List(namespace string, selector labels.Selector) (runtime.Object, error) {
|
||||||
|
return m.RESTClient.Get().
|
||||||
|
Namespace(namespace).
|
||||||
|
Resource(m.Resource).
|
||||||
|
SelectorParam("labels", selector).
|
||||||
|
Do().
|
||||||
|
Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Helper) Watch(namespace, resourceVersion string, labelSelector, fieldSelector labels.Selector) (watch.Interface, error) {
|
||||||
|
return m.RESTClient.Get().
|
||||||
|
Prefix("watch").
|
||||||
|
Namespace(namespace).
|
||||||
|
Resource(m.Resource).
|
||||||
|
Param("resourceVersion", resourceVersion).
|
||||||
|
SelectorParam("labels", labelSelector).
|
||||||
|
SelectorParam("fields", fieldSelector).
|
||||||
|
Watch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Helper) WatchSingle(namespace, name, resourceVersion string) (watch.Interface, error) {
|
||||||
|
return m.RESTClient.Get().
|
||||||
|
Prefix("watch").
|
||||||
|
Namespace(namespace).
|
||||||
|
Resource(m.Resource).
|
||||||
|
Name(name).
|
||||||
|
Param("resourceVersion", resourceVersion).
|
||||||
|
Watch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Helper) Delete(namespace, name string) error {
|
||||||
|
return m.RESTClient.Delete().
|
||||||
|
Namespace(namespace).
|
||||||
|
Resource(m.Resource).
|
||||||
|
Name(name).
|
||||||
|
Do().
|
||||||
|
Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Helper) Create(namespace string, modify bool, data []byte) error {
|
||||||
|
if modify {
|
||||||
|
obj, err := m.Codec.Decode(data)
|
||||||
|
if err != nil {
|
||||||
|
// We don't know how to check a version on this object, but create it anyway
|
||||||
|
return createResource(m.RESTClient, m.Resource, namespace, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to version the object based on client logic.
|
||||||
|
version, err := m.Versioner.ResourceVersion(obj)
|
||||||
|
if err != nil {
|
||||||
|
// We don't know how to clear the version on this object, so send it to the server as is
|
||||||
|
return createResource(m.RESTClient, m.Resource, namespace, data)
|
||||||
|
}
|
||||||
|
if version != "" {
|
||||||
|
if err := m.Versioner.SetResourceVersion(obj, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newData, err := m.Codec.Encode(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = newData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createResource(m.RESTClient, m.Resource, namespace, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createResource(c RESTClient, resource, namespace string, data []byte) error {
|
||||||
|
return c.Post().Namespace(namespace).Resource(resource).Body(data).Do().Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Helper) Update(namespace, name string, overwrite bool, data []byte) error {
|
||||||
|
c := m.RESTClient
|
||||||
|
|
||||||
|
obj, err := m.Codec.Decode(data)
|
||||||
|
if err != nil {
|
||||||
|
// We don't know how to handle this object, but update it anyway
|
||||||
|
return updateResource(c, m.Resource, namespace, name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to version the object based on client logic.
|
||||||
|
version, err := m.Versioner.ResourceVersion(obj)
|
||||||
|
if err != nil {
|
||||||
|
// We don't know how to version this object, so send it to the server as is
|
||||||
|
return updateResource(c, m.Resource, namespace, name, data)
|
||||||
|
}
|
||||||
|
if version == "" && overwrite {
|
||||||
|
// Retrieve the current version of the object to overwrite the server object
|
||||||
|
serverObj, err := c.Get().Namespace(namespace).Resource(m.Resource).Name(name).Do().Get()
|
||||||
|
if err != nil {
|
||||||
|
// The object does not exist, but we want it to be created
|
||||||
|
return updateResource(c, m.Resource, namespace, name, data)
|
||||||
|
}
|
||||||
|
serverVersion, err := m.Versioner.ResourceVersion(serverObj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := m.Versioner.SetResourceVersion(obj, serverVersion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newData, err := m.Codec.Encode(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = newData
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateResource(c, m.Resource, namespace, name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateResource(c RESTClient, resource, namespace, name string, data []byte) error {
|
||||||
|
return c.Put().Namespace(namespace).Resource(resource).Name(name).Body(data).Do().Error()
|
||||||
|
}
|
463
pkg/kubectl/resource/helper_test.go
Normal file
463
pkg/kubectl/resource/helper_test.go
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func objBody(obj runtime.Object) io.ReadCloser {
|
||||||
|
return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(testapi.Codec(), obj))))
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitPath returns the segments for a URL path.
|
||||||
|
func splitPath(path string) []string {
|
||||||
|
path = strings.Trim(path, "/")
|
||||||
|
if path == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
return strings.Split(path, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelperDelete(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Err bool
|
||||||
|
Req func(*http.Request) bool
|
||||||
|
Resp *http.Response
|
||||||
|
HttpErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
HttpErr: errors.New("failure"),
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Resp: &http.Response{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Body: objBody(&api.Status{Status: api.StatusFailure}),
|
||||||
|
},
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Resp: &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: objBody(&api.Status{Status: api.StatusSuccess}),
|
||||||
|
},
|
||||||
|
Req: func(req *http.Request) bool {
|
||||||
|
if req.Method != "DELETE" {
|
||||||
|
t.Errorf("unexpected method: %#v", req)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parts := splitPath(req.URL.Path)
|
||||||
|
if parts[1] != "bar" {
|
||||||
|
t.Errorf("url doesn't contain namespace: %#v", req)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if parts[2] != "foo" {
|
||||||
|
t.Errorf("url doesn't contain name: %#v", req)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
client := &client.FakeRESTClient{
|
||||||
|
Codec: testapi.Codec(),
|
||||||
|
Resp: test.Resp,
|
||||||
|
Err: test.HttpErr,
|
||||||
|
}
|
||||||
|
modifier := &Helper{
|
||||||
|
RESTClient: client,
|
||||||
|
}
|
||||||
|
err := modifier.Delete("bar", "foo")
|
||||||
|
if (err != nil) != test.Err {
|
||||||
|
t.Errorf("unexpected error: %t %v", test.Err, err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if test.Req != nil && !test.Req(client.Req) {
|
||||||
|
t.Errorf("unexpected request: %#v", client.Req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelperCreate(t *testing.T) {
|
||||||
|
expectPost := func(req *http.Request) bool {
|
||||||
|
if req.Method != "POST" {
|
||||||
|
t.Errorf("unexpected method: %#v", req)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parts := splitPath(req.URL.Path)
|
||||||
|
if parts[1] != "bar" {
|
||||||
|
t.Errorf("url doesn't contain namespace: %#v", req)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
Resp *http.Response
|
||||||
|
RespFunc client.HTTPClientFunc
|
||||||
|
HttpErr error
|
||||||
|
Modify bool
|
||||||
|
Object runtime.Object
|
||||||
|
|
||||||
|
ExpectObject runtime.Object
|
||||||
|
Err bool
|
||||||
|
Req func(*http.Request) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
HttpErr: errors.New("failure"),
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Resp: &http.Response{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Body: objBody(&api.Status{Status: api.StatusFailure}),
|
||||||
|
},
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Resp: &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: objBody(&api.Status{Status: api.StatusSuccess}),
|
||||||
|
},
|
||||||
|
Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}},
|
||||||
|
ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}},
|
||||||
|
Req: expectPost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Modify: false,
|
||||||
|
Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
|
||||||
|
ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
|
||||||
|
Resp: &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})},
|
||||||
|
Req: expectPost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Modify: true,
|
||||||
|
Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
|
||||||
|
ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}},
|
||||||
|
Resp: &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})},
|
||||||
|
Req: expectPost,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, test := range tests {
|
||||||
|
client := &client.FakeRESTClient{
|
||||||
|
Codec: testapi.Codec(),
|
||||||
|
Resp: test.Resp,
|
||||||
|
Err: test.HttpErr,
|
||||||
|
}
|
||||||
|
if test.RespFunc != nil {
|
||||||
|
client.Client = test.RespFunc
|
||||||
|
}
|
||||||
|
modifier := &Helper{
|
||||||
|
RESTClient: client,
|
||||||
|
Codec: testapi.Codec(),
|
||||||
|
Versioner: testapi.MetadataAccessor(),
|
||||||
|
}
|
||||||
|
data := []byte{}
|
||||||
|
if test.Object != nil {
|
||||||
|
data = []byte(runtime.EncodeOrDie(testapi.Codec(), test.Object))
|
||||||
|
}
|
||||||
|
err := modifier.Create("bar", test.Modify, data)
|
||||||
|
if (err != nil) != test.Err {
|
||||||
|
t.Errorf("%d: unexpected error: %t %v", i, test.Err, err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if test.Req != nil && !test.Req(client.Req) {
|
||||||
|
t.Errorf("%d: unexpected request: %#v", i, client.Req)
|
||||||
|
}
|
||||||
|
body, err := ioutil.ReadAll(client.Req.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d: unexpected error: %#v", i, err)
|
||||||
|
}
|
||||||
|
t.Logf("got body: %s", string(body))
|
||||||
|
expect := []byte{}
|
||||||
|
if test.ExpectObject != nil {
|
||||||
|
expect = []byte(runtime.EncodeOrDie(testapi.Codec(), test.ExpectObject))
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(expect, body) {
|
||||||
|
t.Errorf("%d: unexpected body: %s", i, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelperGet(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Err bool
|
||||||
|
Req func(*http.Request) bool
|
||||||
|
Resp *http.Response
|
||||||
|
HttpErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
HttpErr: errors.New("failure"),
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Resp: &http.Response{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Body: objBody(&api.Status{Status: api.StatusFailure}),
|
||||||
|
},
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Resp: &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: objBody(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}),
|
||||||
|
},
|
||||||
|
Req: func(req *http.Request) bool {
|
||||||
|
if req.Method != "GET" {
|
||||||
|
t.Errorf("unexpected method: %#v", req)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parts := splitPath(req.URL.Path)
|
||||||
|
if parts[1] != "bar" {
|
||||||
|
t.Errorf("url doesn't contain namespace: %#v", req)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if parts[2] != "foo" {
|
||||||
|
t.Errorf("url doesn't contain name: %#v", req)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
client := &client.FakeRESTClient{
|
||||||
|
Codec: testapi.Codec(),
|
||||||
|
Resp: test.Resp,
|
||||||
|
Err: test.HttpErr,
|
||||||
|
}
|
||||||
|
modifier := &Helper{
|
||||||
|
RESTClient: client,
|
||||||
|
}
|
||||||
|
obj, err := modifier.Get("bar", "foo")
|
||||||
|
if (err != nil) != test.Err {
|
||||||
|
t.Errorf("unexpected error: %t %v", test.Err, err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if obj.(*api.Pod).Name != "foo" {
|
||||||
|
t.Errorf("unexpected object: %#v", obj)
|
||||||
|
}
|
||||||
|
if test.Req != nil && !test.Req(client.Req) {
|
||||||
|
t.Errorf("unexpected request: %#v", client.Req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelperList(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Err bool
|
||||||
|
Req func(*http.Request) bool
|
||||||
|
Resp *http.Response
|
||||||
|
HttpErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
HttpErr: errors.New("failure"),
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Resp: &http.Response{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Body: objBody(&api.Status{Status: api.StatusFailure}),
|
||||||
|
},
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Resp: &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: objBody(&api.PodList{
|
||||||
|
Items: []api.Pod{{
|
||||||
|
ObjectMeta: api.ObjectMeta{Name: "foo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Req: func(req *http.Request) bool {
|
||||||
|
if req.Method != "GET" {
|
||||||
|
t.Errorf("unexpected method: %#v", req)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if req.URL.Path != "/ns/bar" {
|
||||||
|
t.Errorf("url doesn't contain name: %#v", req.URL)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if req.URL.Query().Get("labels") != labels.SelectorFromSet(labels.Set{"foo": "baz"}).String() {
|
||||||
|
t.Errorf("url doesn't contain query parameters: %#v", req.URL)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
client := &client.FakeRESTClient{
|
||||||
|
Codec: testapi.Codec(),
|
||||||
|
Resp: test.Resp,
|
||||||
|
Err: test.HttpErr,
|
||||||
|
}
|
||||||
|
modifier := &Helper{
|
||||||
|
RESTClient: client,
|
||||||
|
}
|
||||||
|
obj, err := modifier.List("bar", labels.SelectorFromSet(labels.Set{"foo": "baz"}))
|
||||||
|
if (err != nil) != test.Err {
|
||||||
|
t.Errorf("unexpected error: %t %v", test.Err, err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if obj.(*api.PodList).Items[0].Name != "foo" {
|
||||||
|
t.Errorf("unexpected object: %#v", obj)
|
||||||
|
}
|
||||||
|
if test.Req != nil && !test.Req(client.Req) {
|
||||||
|
t.Errorf("unexpected request: %#v", client.Req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelperUpdate(t *testing.T) {
|
||||||
|
expectPut := func(req *http.Request) bool {
|
||||||
|
if req.Method != "PUT" {
|
||||||
|
t.Errorf("unexpected method: %#v", req)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parts := splitPath(req.URL.Path)
|
||||||
|
if parts[1] != "bar" {
|
||||||
|
t.Errorf("url doesn't contain namespace: %#v", req.URL)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if parts[2] != "foo" {
|
||||||
|
t.Errorf("url doesn't contain name: %#v", req)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
Resp *http.Response
|
||||||
|
RespFunc client.HTTPClientFunc
|
||||||
|
HttpErr error
|
||||||
|
Overwrite bool
|
||||||
|
Object runtime.Object
|
||||||
|
|
||||||
|
ExpectObject runtime.Object
|
||||||
|
Err bool
|
||||||
|
Req func(*http.Request) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
HttpErr: errors.New("failure"),
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}},
|
||||||
|
Resp: &http.Response{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
Body: objBody(&api.Status{Status: api.StatusFailure}),
|
||||||
|
},
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}},
|
||||||
|
ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}},
|
||||||
|
Resp: &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: objBody(&api.Status{Status: api.StatusSuccess}),
|
||||||
|
},
|
||||||
|
Req: expectPut,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}},
|
||||||
|
ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
|
||||||
|
|
||||||
|
Overwrite: true,
|
||||||
|
RespFunc: func(req *http.Request) (*http.Response, error) {
|
||||||
|
if req.Method == "PUT" {
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})}, nil
|
||||||
|
}
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}})}, nil
|
||||||
|
},
|
||||||
|
Req: expectPut,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Object: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
|
||||||
|
ExpectObject: &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
|
||||||
|
Resp: &http.Response{StatusCode: http.StatusOK, Body: objBody(&api.Status{Status: api.StatusSuccess})},
|
||||||
|
Req: expectPut,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, test := range tests {
|
||||||
|
client := &client.FakeRESTClient{
|
||||||
|
Codec: testapi.Codec(),
|
||||||
|
Resp: test.Resp,
|
||||||
|
Err: test.HttpErr,
|
||||||
|
}
|
||||||
|
if test.RespFunc != nil {
|
||||||
|
client.Client = test.RespFunc
|
||||||
|
}
|
||||||
|
modifier := &Helper{
|
||||||
|
RESTClient: client,
|
||||||
|
Codec: testapi.Codec(),
|
||||||
|
Versioner: testapi.MetadataAccessor(),
|
||||||
|
}
|
||||||
|
data := []byte{}
|
||||||
|
if test.Object != nil {
|
||||||
|
data = []byte(runtime.EncodeOrDie(testapi.Codec(), test.Object))
|
||||||
|
}
|
||||||
|
err := modifier.Update("bar", "foo", test.Overwrite, data)
|
||||||
|
if (err != nil) != test.Err {
|
||||||
|
t.Errorf("%d: unexpected error: %t %v", i, test.Err, err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if test.Req != nil && !test.Req(client.Req) {
|
||||||
|
t.Errorf("%d: unexpected request: %#v", i, client.Req)
|
||||||
|
}
|
||||||
|
body, err := ioutil.ReadAll(client.Req.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%d: unexpected error: %#v", i, err)
|
||||||
|
}
|
||||||
|
t.Logf("got body: %s", string(body))
|
||||||
|
expect := []byte{}
|
||||||
|
if test.ExpectObject != nil {
|
||||||
|
expect = []byte(runtime.EncodeOrDie(testapi.Codec(), test.ExpectObject))
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(expect, body) {
|
||||||
|
t.Errorf("%d: unexpected body: %s", i, string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
pkg/kubectl/resource/interfaces.go
Normal file
44
pkg/kubectl/resource/interfaces.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RESTClient is a client helper for dealing with RESTful resources
|
||||||
|
// in a generic way.
|
||||||
|
type RESTClient interface {
|
||||||
|
Get() *client.Request
|
||||||
|
Post() *client.Request
|
||||||
|
Delete() *client.Request
|
||||||
|
Put() *client.Request
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientMapper retrieves a client object for a given mapping
|
||||||
|
type ClientMapper interface {
|
||||||
|
ClientForMapping(mapping *meta.RESTMapping) (RESTClient, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientMapperFunc implements ClientMapper for a function
|
||||||
|
type ClientMapperFunc func(mapping *meta.RESTMapping) (RESTClient, error)
|
||||||
|
|
||||||
|
// ClientForMapping implements ClientMapper
|
||||||
|
func (f ClientMapperFunc) ClientForMapping(mapping *meta.RESTMapping) (RESTClient, error) {
|
||||||
|
return f(mapping)
|
||||||
|
}
|
97
pkg/kubectl/resource/mapper.go
Normal file
97
pkg/kubectl/resource/mapper.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mapper is a convenience struct for holding references to the three interfaces
|
||||||
|
// needed to create Info for arbitrary objects.
|
||||||
|
type Mapper struct {
|
||||||
|
runtime.ObjectTyper
|
||||||
|
meta.RESTMapper
|
||||||
|
ClientMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoForData creates an Info object for the given data. An error is returned
|
||||||
|
// if any of the decoding or client lookup steps fail. Name and namespace will be
|
||||||
|
// set into Info if the mapping's MetadataAccessor can retrieve them.
|
||||||
|
func (m *Mapper) InfoForData(data []byte, source string) (*Info, error) {
|
||||||
|
version, kind, err := m.DataVersionAndKind(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get type info from %q: %v", source, err)
|
||||||
|
}
|
||||||
|
mapping, err := m.RESTMapping(kind, version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to recognize %q: %v", source, err)
|
||||||
|
}
|
||||||
|
obj, err := mapping.Codec.Decode(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to load %q: %v", source, err)
|
||||||
|
}
|
||||||
|
client, err := m.ClientForMapping(mapping)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to connect to a server to handle %q: %v", mapping.Resource, err)
|
||||||
|
}
|
||||||
|
name, _ := mapping.MetadataAccessor.Name(obj)
|
||||||
|
namespace, _ := mapping.MetadataAccessor.Namespace(obj)
|
||||||
|
resourceVersion, _ := mapping.MetadataAccessor.ResourceVersion(obj)
|
||||||
|
return &Info{
|
||||||
|
Mapping: mapping,
|
||||||
|
Client: client,
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: name,
|
||||||
|
|
||||||
|
Object: obj,
|
||||||
|
ResourceVersion: resourceVersion,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoForData creates an Info object for the given Object. An error is returned
|
||||||
|
// if the object cannot be introspected. Name and namespace will be set into Info
|
||||||
|
// if the mapping's MetadataAccessor can retrieve them.
|
||||||
|
func (m *Mapper) InfoForObject(obj runtime.Object) (*Info, error) {
|
||||||
|
version, kind, err := m.ObjectVersionAndKind(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get type info from the object %q: %v", reflect.TypeOf(obj), err)
|
||||||
|
}
|
||||||
|
mapping, err := m.RESTMapping(kind, version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to recognize %q: %v", kind, err)
|
||||||
|
}
|
||||||
|
client, err := m.ClientForMapping(mapping)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to connect to a server to handle %q: %v", mapping.Resource, err)
|
||||||
|
}
|
||||||
|
name, _ := mapping.MetadataAccessor.Name(obj)
|
||||||
|
namespace, _ := mapping.MetadataAccessor.Namespace(obj)
|
||||||
|
resourceVersion, _ := mapping.MetadataAccessor.ResourceVersion(obj)
|
||||||
|
return &Info{
|
||||||
|
Mapping: mapping,
|
||||||
|
Client: client,
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: name,
|
||||||
|
|
||||||
|
Object: obj,
|
||||||
|
ResourceVersion: resourceVersion,
|
||||||
|
}, nil
|
||||||
|
}
|
80
pkg/kubectl/resource/selector.go
Normal file
80
pkg/kubectl/resource/selector.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Selector is a Visitor for resources that match a label selector.
|
||||||
|
type Selector struct {
|
||||||
|
Client RESTClient
|
||||||
|
Mapping *meta.RESTMapping
|
||||||
|
Namespace string
|
||||||
|
Selector labels.Selector
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSelector creates a resource selector which hides details of getting items by their label selector.
|
||||||
|
func NewSelector(client RESTClient, mapping *meta.RESTMapping, namespace string, selector labels.Selector) *Selector {
|
||||||
|
return &Selector{
|
||||||
|
Client: client,
|
||||||
|
Mapping: mapping,
|
||||||
|
Namespace: namespace,
|
||||||
|
Selector: selector,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit implements Visitor
|
||||||
|
func (r *Selector) Visit(fn VisitorFunc) error {
|
||||||
|
list, err := NewHelper(r.Client, r.Mapping).List(r.Namespace, r.Selector)
|
||||||
|
if err != nil {
|
||||||
|
if errors.IsBadRequest(err) || errors.IsNotFound(err) {
|
||||||
|
if r.Selector.Empty() {
|
||||||
|
glog.V(2).Infof("Unable to list %q: %v", r.Mapping.Resource, err)
|
||||||
|
} else {
|
||||||
|
glog.V(2).Infof("Unable to find %q that match the selector %q: %v", r.Mapping.Resource, r.Selector, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
accessor := r.Mapping.MetadataAccessor
|
||||||
|
resourceVersion, _ := accessor.ResourceVersion(list)
|
||||||
|
info := &Info{
|
||||||
|
Client: r.Client,
|
||||||
|
Mapping: r.Mapping,
|
||||||
|
Namespace: r.Namespace,
|
||||||
|
|
||||||
|
Object: list,
|
||||||
|
ResourceVersion: resourceVersion,
|
||||||
|
}
|
||||||
|
return fn(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Selector) Watch(resourceVersion string) (watch.Interface, error) {
|
||||||
|
return NewHelper(r.Client, r.Mapping).Watch(r.Namespace, resourceVersion, r.Selector, labels.Everything())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceMapping returns the mapping for this resource and implements ResourceMapping
|
||||||
|
func (r *Selector) ResourceMapping() *meta.RESTMapping {
|
||||||
|
return r.Mapping
|
||||||
|
}
|
422
pkg/kubectl/resource/visitor.go
Normal file
422
pkg/kubectl/resource/visitor.go
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Visitor lets clients walk a list of resources.
|
||||||
|
type Visitor interface {
|
||||||
|
Visit(VisitorFunc) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisitorFunc implements the Visitor interface for a matching function
|
||||||
|
type VisitorFunc func(*Info) error
|
||||||
|
|
||||||
|
// Watchable describes a resource that can be watched for changes that occur on the server,
|
||||||
|
// beginning after the provided resource version.
|
||||||
|
type Watchable interface {
|
||||||
|
Watch(resourceVersion string) (watch.Interface, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceMapping allows an object to return the resource mapping associated with
|
||||||
|
// the resource or resources it represents.
|
||||||
|
type ResourceMapping interface {
|
||||||
|
ResourceMapping() *meta.RESTMapping
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info contains temporary info to execute a REST call, or show the results
|
||||||
|
// of an already completed REST call.
|
||||||
|
type Info struct {
|
||||||
|
Client RESTClient
|
||||||
|
Mapping *meta.RESTMapping
|
||||||
|
Namespace string
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Optional, this is the most recent value returned by the server if available
|
||||||
|
runtime.Object
|
||||||
|
// Optional, this is the most recent resource version the server knows about for
|
||||||
|
// this type of resource. It may not match the resource version of the object,
|
||||||
|
// but if set it should be equal to or newer than the resource version of the
|
||||||
|
// object (however the server defines resource version).
|
||||||
|
ResourceVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInfo returns a new info object
|
||||||
|
func NewInfo(client RESTClient, mapping *meta.RESTMapping, namespace, name string) *Info {
|
||||||
|
return &Info{
|
||||||
|
Client: client,
|
||||||
|
Mapping: mapping,
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit implements Visitor
|
||||||
|
func (i *Info) Visit(fn VisitorFunc) error {
|
||||||
|
return fn(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Info) Get() error {
|
||||||
|
obj, err := NewHelper(i.Client, i.Mapping).Get(i.Namespace, i.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Object = obj
|
||||||
|
i.ResourceVersion, _ = i.Mapping.MetadataAccessor.ResourceVersion(obj)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceMapping returns the mapping for this resource and implements ResourceMapping
|
||||||
|
func (i *Info) ResourceMapping() *meta.RESTMapping {
|
||||||
|
return i.Mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisitorList implements Visit for the sub visitors it contains. The first error
|
||||||
|
// returned from a child Visitor will terminate iteration.
|
||||||
|
type VisitorList []Visitor
|
||||||
|
|
||||||
|
// Visit implements Visitor
|
||||||
|
func (l VisitorList) Visit(fn VisitorFunc) error {
|
||||||
|
for i := range l {
|
||||||
|
if err := l[i].Visit(fn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EagerVisitorList implements Visit for the sub visitors it contains. All errors
|
||||||
|
// will be captured and returned at the end of iteration.
|
||||||
|
type EagerVisitorList []Visitor
|
||||||
|
|
||||||
|
// Visit implements Visitor, and gathers errors that occur during processing until
|
||||||
|
// all sub visitors have been visited.
|
||||||
|
func (l EagerVisitorList) Visit(fn VisitorFunc) error {
|
||||||
|
errs := []error(nil)
|
||||||
|
for i := range l {
|
||||||
|
if err := l[i].Visit(func(info *Info) error {
|
||||||
|
if err := fn(info); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.NewAggregate(errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathVisitor visits a given path and returns an object representing the file
|
||||||
|
// at that path.
|
||||||
|
type PathVisitor struct {
|
||||||
|
*Mapper
|
||||||
|
// The file path to load
|
||||||
|
Path string
|
||||||
|
// Whether to ignore files that are not recognized as API objects
|
||||||
|
IgnoreErrors bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *PathVisitor) Visit(fn VisitorFunc) error {
|
||||||
|
data, err := ioutil.ReadFile(v.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to read %q: %v", v.Path, err)
|
||||||
|
}
|
||||||
|
info, err := v.Mapper.InfoForData(data, v.Path)
|
||||||
|
if err != nil {
|
||||||
|
if v.IgnoreErrors {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
glog.V(2).Infof("Unable to load file %q: %v", v.Path, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fn(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirectoryVisitor loads the specified files from a directory and passes them
|
||||||
|
// to visitors.
|
||||||
|
type DirectoryVisitor struct {
|
||||||
|
*Mapper
|
||||||
|
// The directory or file to start from
|
||||||
|
Path string
|
||||||
|
// Whether directories are recursed
|
||||||
|
Recursive bool
|
||||||
|
// The file extensions to include. If empty, all files are read.
|
||||||
|
Extensions []string
|
||||||
|
// Whether to ignore files that are not recognized as API objects
|
||||||
|
IgnoreErrors bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *DirectoryVisitor) ignoreFile(path string) bool {
|
||||||
|
if len(v.Extensions) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
for _, s := range v.Extensions {
|
||||||
|
if s == ext {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *DirectoryVisitor) Visit(fn VisitorFunc) error {
|
||||||
|
return filepath.Walk(v.Path, func(path string, fi os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.IsDir() {
|
||||||
|
if path != v.Path && !v.Recursive {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if v.ignoreFile(path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to read %q: %v", path, err)
|
||||||
|
}
|
||||||
|
info, err := v.Mapper.InfoForData(data, path)
|
||||||
|
if err != nil {
|
||||||
|
if v.IgnoreErrors {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
glog.V(2).Infof("Unable to load file %q: %v", path, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fn(info)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// URLVisitor downloads the contents of a URL, and if successful, returns
|
||||||
|
// an info object representing the downloaded object.
|
||||||
|
type URLVisitor struct {
|
||||||
|
*Mapper
|
||||||
|
URL *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *URLVisitor) Visit(fn VisitorFunc) error {
|
||||||
|
res, err := http.Get(v.URL.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to access URL %q: %v\n", v.URL, err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("unable to read URL %q, server reported %d %s", v.URL, res.StatusCode, res.Status)
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to read URL %q: %v\n", v.URL, err)
|
||||||
|
}
|
||||||
|
info, err := v.Mapper.InfoForData(data, v.URL.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fn(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecoratedVisitor will invoke the decorators in order prior to invoking the visitor function
|
||||||
|
// passed to Visit. An error will terminate the visit.
|
||||||
|
type DecoratedVisitor struct {
|
||||||
|
visitor Visitor
|
||||||
|
decorators []VisitorFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDecoratedVisitor will create a visitor that invokes the provided visitor functions before
|
||||||
|
// the user supplied visitor function is invoked, giving them the opportunity to mutate the Info
|
||||||
|
// object or terminate early with an error.
|
||||||
|
func NewDecoratedVisitor(v Visitor, fn ...VisitorFunc) Visitor {
|
||||||
|
if len(fn) == 0 {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return DecoratedVisitor{v, fn}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit implements Visitor
|
||||||
|
func (v DecoratedVisitor) Visit(fn VisitorFunc) error {
|
||||||
|
return v.visitor.Visit(func(info *Info) error {
|
||||||
|
for i := range v.decorators {
|
||||||
|
if err := v.decorators[i](info); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fn(info)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlattenListVisitor flattens any objects that runtime.ExtractList recognizes as a list
|
||||||
|
// - has an "Items" public field that is a slice of runtime.Objects or objects satisfying
|
||||||
|
// that interface - into multiple Infos. An error on any sub item (for instance, if a List
|
||||||
|
// contains an object that does not have a registered client or resource) will terminate
|
||||||
|
// the visit.
|
||||||
|
// TODO: allow errors to be aggregated?
|
||||||
|
type FlattenListVisitor struct {
|
||||||
|
Visitor
|
||||||
|
*Mapper
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFlattenListVisitor creates a visitor that will expand list style runtime.Objects
|
||||||
|
// into individual items and then visit them individually.
|
||||||
|
func NewFlattenListVisitor(v Visitor, mapper *Mapper) Visitor {
|
||||||
|
return FlattenListVisitor{v, mapper}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v FlattenListVisitor) Visit(fn VisitorFunc) error {
|
||||||
|
return v.Visitor.Visit(func(info *Info) error {
|
||||||
|
if info.Object == nil {
|
||||||
|
return fn(info)
|
||||||
|
}
|
||||||
|
items, err := runtime.ExtractList(info.Object)
|
||||||
|
if err != nil {
|
||||||
|
return fn(info)
|
||||||
|
}
|
||||||
|
for i := range items {
|
||||||
|
item, err := v.InfoForObject(items[i])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(info.ResourceVersion) != 0 {
|
||||||
|
item.ResourceVersion = info.ResourceVersion
|
||||||
|
}
|
||||||
|
if err := fn(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamVisitor reads objects from an io.Reader and walks them. A stream visitor can only be
|
||||||
|
// visited once.
|
||||||
|
// TODO: depends on objects being in JSON format before being passed to decode - need to implement
|
||||||
|
// a stream decoder method on runtime.Codec to properly handle this.
|
||||||
|
type StreamVisitor struct {
|
||||||
|
io.Reader
|
||||||
|
*Mapper
|
||||||
|
|
||||||
|
Source string
|
||||||
|
IgnoreErrors bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStreamVisitor creates a visitor that will return resources that were encoded into the provided
|
||||||
|
// stream. If ignoreErrors is set, unrecognized or invalid objects will be skipped and logged.
|
||||||
|
func NewStreamVisitor(r io.Reader, mapper *Mapper, source string, ignoreErrors bool) Visitor {
|
||||||
|
return &StreamVisitor{r, mapper, source, ignoreErrors}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit implements Visitor over a stream.
|
||||||
|
func (v *StreamVisitor) Visit(fn VisitorFunc) error {
|
||||||
|
d := json.NewDecoder(v.Reader)
|
||||||
|
for {
|
||||||
|
ext := runtime.RawExtension{}
|
||||||
|
if err := d.Decode(&ext); err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info, err := v.InfoForData(ext.RawJSON, v.Source)
|
||||||
|
if err != nil {
|
||||||
|
if v.IgnoreErrors {
|
||||||
|
glog.V(2).Infof("Unable to read item from stream %q: %v", err)
|
||||||
|
glog.V(4).Infof("Unreadable: %s", string(ext.RawJSON))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fn(info); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateObjectNamespace(info *Info) error {
|
||||||
|
if info.Object != nil {
|
||||||
|
return info.Mapping.MetadataAccessor.SetNamespace(info.Object, info.Namespace)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNamespace ensures that every Info object visited will have a namespace
|
||||||
|
// set. If info.Object is set, it will be mutated as well.
|
||||||
|
func SetNamespace(namespace string) VisitorFunc {
|
||||||
|
return func(info *Info) error {
|
||||||
|
if len(info.Namespace) == 0 {
|
||||||
|
info.Namespace = namespace
|
||||||
|
UpdateObjectNamespace(info)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireNamespace will either set a namespace if none is provided on the
|
||||||
|
// Info object, or if the namespace is set and does not match the provided
|
||||||
|
// value, returns an error. This is intended to guard against administrators
|
||||||
|
// accidentally operating on resources outside their namespace.
|
||||||
|
func RequireNamespace(namespace string) VisitorFunc {
|
||||||
|
return func(info *Info) error {
|
||||||
|
if len(info.Namespace) == 0 {
|
||||||
|
info.Namespace = namespace
|
||||||
|
UpdateObjectNamespace(info)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if info.Namespace != namespace {
|
||||||
|
return fmt.Errorf("the namespace from the provided object %q does not match the namespace %q. You must pass '--namespace=%s' to perform this operation.", info.Namespace, namespace, info.Namespace)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
obj, err := NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info.Object = obj
|
||||||
|
info.ResourceVersion, _ = info.Mapping.MetadataAccessor.ResourceVersion(obj)
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user