working commit
This commit is contained in:
+228
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
// ResourceBuilderFlags are flags for finding resources
|
||||
// TODO(juanvallejo): wire --local flag from commands through
|
||||
type ResourceBuilderFlags struct {
|
||||
FileNameFlags *FileNameFlags
|
||||
|
||||
LabelSelector *string
|
||||
FieldSelector *string
|
||||
AllNamespaces *bool
|
||||
All *bool
|
||||
Local *bool
|
||||
|
||||
Scheme *runtime.Scheme
|
||||
Latest bool
|
||||
StopOnFirstError bool
|
||||
}
|
||||
|
||||
// NewResourceBuilderFlags returns a default ResourceBuilderFlags
|
||||
func NewResourceBuilderFlags() *ResourceBuilderFlags {
|
||||
filenames := []string{}
|
||||
|
||||
return &ResourceBuilderFlags{
|
||||
FileNameFlags: &FileNameFlags{
|
||||
Usage: "identifying the resource.",
|
||||
Filenames: &filenames,
|
||||
Recursive: ptr.To(true),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// WithFile sets the FileNameFlags.
|
||||
// If recurse is set, it will process directory recursively. Useful when you want to manage related manifests
|
||||
// organized within the same directory.
|
||||
func (o *ResourceBuilderFlags) WithFile(recurse bool, files ...string) *ResourceBuilderFlags {
|
||||
o.FileNameFlags = &FileNameFlags{
|
||||
Usage: "identifying the resource.",
|
||||
Filenames: &files,
|
||||
Recursive: ptr.To(recurse),
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
// WithLabelSelector sets the LabelSelector flag
|
||||
func (o *ResourceBuilderFlags) WithLabelSelector(selector string) *ResourceBuilderFlags {
|
||||
o.LabelSelector = &selector
|
||||
return o
|
||||
}
|
||||
|
||||
// WithFieldSelector sets the FieldSelector flag
|
||||
func (o *ResourceBuilderFlags) WithFieldSelector(selector string) *ResourceBuilderFlags {
|
||||
o.FieldSelector = &selector
|
||||
return o
|
||||
}
|
||||
|
||||
// WithAllNamespaces sets the AllNamespaces flag
|
||||
func (o *ResourceBuilderFlags) WithAllNamespaces(defaultVal bool) *ResourceBuilderFlags {
|
||||
o.AllNamespaces = &defaultVal
|
||||
return o
|
||||
}
|
||||
|
||||
// WithAll sets the All flag
|
||||
func (o *ResourceBuilderFlags) WithAll(defaultVal bool) *ResourceBuilderFlags {
|
||||
o.All = &defaultVal
|
||||
return o
|
||||
}
|
||||
|
||||
// WithLocal sets the Local flag
|
||||
func (o *ResourceBuilderFlags) WithLocal(defaultVal bool) *ResourceBuilderFlags {
|
||||
o.Local = &defaultVal
|
||||
return o
|
||||
}
|
||||
|
||||
// WithScheme sets the Scheme flag
|
||||
func (o *ResourceBuilderFlags) WithScheme(scheme *runtime.Scheme) *ResourceBuilderFlags {
|
||||
o.Scheme = scheme
|
||||
return o
|
||||
}
|
||||
|
||||
// WithLatest sets the Latest flag
|
||||
func (o *ResourceBuilderFlags) WithLatest() *ResourceBuilderFlags {
|
||||
o.Latest = true
|
||||
return o
|
||||
}
|
||||
|
||||
// StopOnError sets the StopOnFirstError flag
|
||||
func (o *ResourceBuilderFlags) StopOnError() *ResourceBuilderFlags {
|
||||
o.StopOnFirstError = true
|
||||
return o
|
||||
}
|
||||
|
||||
// AddFlags registers flags for finding resources
|
||||
func (o *ResourceBuilderFlags) AddFlags(flagset *pflag.FlagSet) {
|
||||
o.FileNameFlags.AddFlags(flagset)
|
||||
|
||||
if o.LabelSelector != nil {
|
||||
flagset.StringVarP(o.LabelSelector, "selector", "l", *o.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
|
||||
}
|
||||
if o.FieldSelector != nil {
|
||||
flagset.StringVar(o.FieldSelector, "field-selector", *o.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.")
|
||||
}
|
||||
if o.AllNamespaces != nil {
|
||||
flagset.BoolVarP(o.AllNamespaces, "all-namespaces", "A", *o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
|
||||
}
|
||||
if o.All != nil {
|
||||
flagset.BoolVar(o.All, "all", *o.All, "Select all resources in the namespace of the specified resource types")
|
||||
}
|
||||
if o.Local != nil {
|
||||
flagset.BoolVar(o.Local, "local", *o.Local, "If true, annotation will NOT contact api-server but run locally.")
|
||||
}
|
||||
}
|
||||
|
||||
// ToBuilder gives you back a resource finder to visit resources that are located
|
||||
func (o *ResourceBuilderFlags) ToBuilder(restClientGetter RESTClientGetter, resources []string) ResourceFinder {
|
||||
namespace, enforceNamespace, namespaceErr := restClientGetter.ToRawKubeConfigLoader().Namespace()
|
||||
|
||||
builder := resource.NewBuilder(restClientGetter).
|
||||
NamespaceParam(namespace).DefaultNamespace()
|
||||
|
||||
if o.AllNamespaces != nil {
|
||||
builder.AllNamespaces(*o.AllNamespaces)
|
||||
}
|
||||
|
||||
if o.Scheme != nil {
|
||||
builder.WithScheme(o.Scheme, o.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
} else {
|
||||
builder.Unstructured()
|
||||
}
|
||||
|
||||
if o.FileNameFlags != nil {
|
||||
opts := o.FileNameFlags.ToOptions()
|
||||
builder.FilenameParam(enforceNamespace, &opts)
|
||||
}
|
||||
|
||||
if o.Local == nil || !*o.Local {
|
||||
// resource type/name tuples only work non-local
|
||||
if o.All != nil {
|
||||
builder.ResourceTypeOrNameArgs(*o.All, resources...)
|
||||
} else {
|
||||
builder.ResourceTypeOrNameArgs(false, resources...)
|
||||
}
|
||||
// label selectors only work non-local (for now)
|
||||
if o.LabelSelector != nil {
|
||||
builder.LabelSelectorParam(*o.LabelSelector)
|
||||
}
|
||||
// field selectors only work non-local (forever)
|
||||
if o.FieldSelector != nil {
|
||||
builder.FieldSelectorParam(*o.FieldSelector)
|
||||
}
|
||||
// latest only works non-local (forever)
|
||||
if o.Latest {
|
||||
builder.Latest()
|
||||
}
|
||||
|
||||
} else {
|
||||
builder.Local()
|
||||
|
||||
if len(resources) > 0 {
|
||||
builder.AddError(resource.LocalResourceError)
|
||||
}
|
||||
}
|
||||
|
||||
if !o.StopOnFirstError {
|
||||
builder.ContinueOnError()
|
||||
}
|
||||
|
||||
return &ResourceFindBuilderWrapper{
|
||||
builder: builder.
|
||||
Flatten(). // I think we're going to recommend this everywhere
|
||||
AddError(namespaceErr),
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceFindBuilderWrapper wraps a builder in an interface
|
||||
type ResourceFindBuilderWrapper struct {
|
||||
builder *resource.Builder
|
||||
}
|
||||
|
||||
// Do finds you resources to check
|
||||
func (b *ResourceFindBuilderWrapper) Do() resource.Visitor {
|
||||
return b.builder.Do()
|
||||
}
|
||||
|
||||
// ResourceFinder allows mocking the resource builder
|
||||
// TODO resource builders needs to become more interfacey
|
||||
type ResourceFinder interface {
|
||||
Do() resource.Visitor
|
||||
}
|
||||
|
||||
// ResourceFinderFunc is a handy way to make a ResourceFinder
|
||||
type ResourceFinderFunc func() resource.Visitor
|
||||
|
||||
// Do implements ResourceFinder
|
||||
func (fn ResourceFinderFunc) Do() resource.Visitor {
|
||||
return fn()
|
||||
}
|
||||
|
||||
// ResourceFinderForResult skins a visitor for re-use as a ResourceFinder
|
||||
func ResourceFinderForResult(result resource.Visitor) ResourceFinder {
|
||||
return ResourceFinderFunc(func() resource.Visitor {
|
||||
return result
|
||||
})
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// NewSimpleFakeResourceFinder builds a super simple ResourceFinder that just iterates over the objects you provided
|
||||
func NewSimpleFakeResourceFinder(infos ...*resource.Info) *FakeResourceFinder {
|
||||
return &FakeResourceFinder{
|
||||
Infos: infos,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FakeResourceFinder) WithError(err error) *FakeResourceFinder {
|
||||
f.err = err
|
||||
return f
|
||||
}
|
||||
|
||||
type FakeResourceFinder struct {
|
||||
Infos []*resource.Info
|
||||
err error
|
||||
}
|
||||
|
||||
// Do implements the interface
|
||||
func (f *FakeResourceFinder) Do() resource.Visitor {
|
||||
return &fakeResourceResult{
|
||||
Infos: f.Infos,
|
||||
err: f.err,
|
||||
}
|
||||
}
|
||||
|
||||
type fakeResourceResult struct {
|
||||
Infos []*resource.Info
|
||||
err error
|
||||
}
|
||||
|
||||
// Visit just iterates over info
|
||||
func (r *fakeResourceResult) Visit(fn resource.VisitorFunc) error {
|
||||
if r.err != nil {
|
||||
return r.err
|
||||
}
|
||||
for _, info := range r.Infos {
|
||||
err := fn(info, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2020 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrEmptyConfig is the error message to be displayed if the configuration info is missing or incomplete
|
||||
ErrEmptyConfig = clientcmd.NewEmptyConfigError(`Missing or incomplete configuration info. Please point to an existing, complete config file:
|
||||
|
||||
|
||||
1. Via the command-line flag --kubeconfig
|
||||
2. Via the KUBECONFIG environment variable
|
||||
3. In your home directory as ~/.kube/config
|
||||
|
||||
To view or setup config directly use the 'config' command.`)
|
||||
)
|
||||
|
||||
var _ = clientcmd.ClientConfig(&clientConfig{})
|
||||
|
||||
type clientConfig struct {
|
||||
defaultClientConfig clientcmd.ClientConfig
|
||||
}
|
||||
|
||||
func (c *clientConfig) RawConfig() (clientcmdapi.Config, error) {
|
||||
config, err := c.defaultClientConfig.RawConfig()
|
||||
// replace client-go's ErrEmptyConfig error with our custom, more verbose version
|
||||
if clientcmd.IsEmptyConfig(err) {
|
||||
return config, ErrEmptyConfig
|
||||
}
|
||||
return config, err
|
||||
}
|
||||
|
||||
func (c *clientConfig) ClientConfig() (*restclient.Config, error) {
|
||||
config, err := c.defaultClientConfig.ClientConfig()
|
||||
// replace client-go's ErrEmptyConfig error with our custom, more verbose version
|
||||
if clientcmd.IsEmptyConfig(err) {
|
||||
return config, ErrEmptyConfig
|
||||
}
|
||||
return config, err
|
||||
}
|
||||
|
||||
func (c *clientConfig) Namespace() (string, bool, error) {
|
||||
namespace, ok, err := c.defaultClientConfig.Namespace()
|
||||
// replace client-go's ErrEmptyConfig error with our custom, more verbose version
|
||||
if clientcmd.IsEmptyConfig(err) {
|
||||
return namespace, ok, ErrEmptyConfig
|
||||
}
|
||||
return namespace, ok, err
|
||||
}
|
||||
|
||||
func (c *clientConfig) ConfigAccess() clientcmd.ConfigAccess {
|
||||
return c.defaultClientConfig.ConfigAccess()
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
kubectlCommandHeader = "Kubectl-Command"
|
||||
kubectlSessionHeader = "Kubectl-Session"
|
||||
)
|
||||
|
||||
// CommandHeaderRoundTripper adds a layer around the standard
|
||||
// round tripper to add Request headers before delegation. Implements
|
||||
// the go standard library "http.RoundTripper" interface.
|
||||
type CommandHeaderRoundTripper struct {
|
||||
Delegate http.RoundTripper
|
||||
Headers map[string]string
|
||||
SkipHeaders *atomic.Bool
|
||||
}
|
||||
|
||||
// CommandHeaderRoundTripper adds Request headers before delegating to standard
|
||||
// round tripper. These headers are kubectl command headers which
|
||||
// detail the kubectl command. See SIG CLI KEP 859:
|
||||
//
|
||||
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/859-kubectl-headers
|
||||
func (c *CommandHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if c.shouldSkipHeaders() {
|
||||
return c.Delegate.RoundTrip(req)
|
||||
}
|
||||
|
||||
for header, value := range c.Headers {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
|
||||
return c.Delegate.RoundTrip(req)
|
||||
}
|
||||
|
||||
// ParseCommandHeaders fills in a map of custom headers into the CommandHeaderRoundTripper. These
|
||||
// headers are then filled into each request. For details on the custom headers see:
|
||||
//
|
||||
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/859-kubectl-headers
|
||||
//
|
||||
// Each call overwrites the previously parsed command headers (not additive).
|
||||
// TODO(seans3): Parse/add flags removing PII from flag values.
|
||||
func (c *CommandHeaderRoundTripper) ParseCommandHeaders(cmd *cobra.Command, args []string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
// Overwrites previously parsed command headers (headers not additive).
|
||||
c.Headers = map[string]string{}
|
||||
// Session identifier to aggregate multiple Requests from single kubectl command.
|
||||
uid := uuid.New().String()
|
||||
c.Headers[kubectlSessionHeader] = uid
|
||||
// Iterate up the hierarchy of commands from the leaf command to create
|
||||
// the full command string. Example: kubectl create secret generic
|
||||
cmdStrs := []string{}
|
||||
for cmd.HasParent() {
|
||||
parent := cmd.Parent()
|
||||
currName := strings.TrimSpace(cmd.Name())
|
||||
cmdStrs = append([]string{currName}, cmdStrs...)
|
||||
cmd = parent
|
||||
}
|
||||
currName := strings.TrimSpace(cmd.Name())
|
||||
cmdStrs = append([]string{currName}, cmdStrs...)
|
||||
if len(cmdStrs) > 0 {
|
||||
c.Headers[kubectlCommandHeader] = strings.Join(cmdStrs, " ")
|
||||
}
|
||||
}
|
||||
|
||||
// CancelRequest is propagated to the Delegate RoundTripper within
|
||||
// if the wrapped RoundTripper implements this function.
|
||||
func (c *CommandHeaderRoundTripper) CancelRequest(req *http.Request) {
|
||||
type canceler interface {
|
||||
CancelRequest(*http.Request)
|
||||
}
|
||||
// If possible, call "CancelRequest" on the wrapped Delegate RoundTripper.
|
||||
if cr, ok := c.Delegate.(canceler); ok {
|
||||
cr.CancelRequest(req)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommandHeaderRoundTripper) shouldSkipHeaders() bool {
|
||||
if c.SkipHeaders == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return c.SkipHeaders.Load()
|
||||
}
|
||||
+514
@@ -0,0 +1,514 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
"k8s.io/client-go/discovery"
|
||||
diskcached "k8s.io/client-go/discovery/cached/disk"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
const (
|
||||
flagClusterName = "cluster"
|
||||
flagAuthInfoName = "user"
|
||||
flagContext = "context"
|
||||
flagNamespace = "namespace"
|
||||
flagAPIServer = "server"
|
||||
flagTLSServerName = "tls-server-name"
|
||||
flagInsecure = "insecure-skip-tls-verify"
|
||||
flagCertFile = "client-certificate"
|
||||
flagKeyFile = "client-key"
|
||||
flagCAFile = "certificate-authority"
|
||||
flagBearerToken = "token"
|
||||
flagImpersonate = "as"
|
||||
flagImpersonateUID = "as-uid"
|
||||
flagImpersonateGroup = "as-group"
|
||||
flagImpersonateUserExtra = "as-user-extra"
|
||||
flagUsername = "username"
|
||||
flagPassword = "password"
|
||||
flagTimeout = "request-timeout"
|
||||
flagCacheDir = "cache-dir"
|
||||
flagDisableCompression = "disable-compression"
|
||||
)
|
||||
|
||||
// RESTClientGetter is an interface that the ConfigFlags describe to provide an easier way to mock for commands
|
||||
// and eliminate the direct coupling to a struct type. Users may wish to duplicate this type in their own packages
|
||||
// as per the golang type overlapping.
|
||||
type RESTClientGetter interface {
|
||||
// ToRESTConfig returns restconfig
|
||||
ToRESTConfig() (*rest.Config, error)
|
||||
// ToDiscoveryClient returns discovery client
|
||||
ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
|
||||
// ToRESTMapper returns a restmapper
|
||||
ToRESTMapper() (meta.RESTMapper, error)
|
||||
// ToRawKubeConfigLoader return kubeconfig loader as-is
|
||||
ToRawKubeConfigLoader() clientcmd.ClientConfig
|
||||
}
|
||||
|
||||
var _ RESTClientGetter = &ConfigFlags{}
|
||||
|
||||
// ConfigFlags composes the set of values necessary
|
||||
// for obtaining a REST client config
|
||||
type ConfigFlags struct {
|
||||
CacheDir *string
|
||||
KubeConfig *string
|
||||
|
||||
// config flags
|
||||
ClusterName *string
|
||||
AuthInfoName *string
|
||||
Context *string
|
||||
Namespace *string
|
||||
APIServer *string
|
||||
TLSServerName *string
|
||||
Insecure *bool
|
||||
CertFile *string
|
||||
KeyFile *string
|
||||
CAFile *string
|
||||
BearerToken *string
|
||||
Impersonate *string
|
||||
ImpersonateUID *string
|
||||
ImpersonateGroup *[]string
|
||||
ImpersonateUserExtra *[]string
|
||||
Username *string
|
||||
Password *string
|
||||
Timeout *string
|
||||
DisableCompression *bool
|
||||
// If non-nil, wrap config function can transform the Config
|
||||
// before it is returned in ToRESTConfig function.
|
||||
WrapConfigFn func(*rest.Config) *rest.Config
|
||||
|
||||
clientConfig clientcmd.ClientConfig
|
||||
clientConfigLock sync.Mutex
|
||||
|
||||
restMapper meta.RESTMapper
|
||||
restMapperLock sync.Mutex
|
||||
|
||||
discoveryClient discovery.CachedDiscoveryInterface
|
||||
discoveryClientLock sync.Mutex
|
||||
|
||||
// If set to true, will use persistent client config, rest mapper, discovery client, and
|
||||
// propagate them to the places that need them, rather than
|
||||
// instantiating them multiple times.
|
||||
usePersistentConfig bool
|
||||
// Allows increasing burst used for discovery, this is useful
|
||||
// in clusters with many registered resources
|
||||
discoveryBurst int
|
||||
// Allows increasing qps used for discovery, this is useful
|
||||
// in clusters with many registered resources
|
||||
discoveryQPS float32
|
||||
// Allows all possible warnings are printed in a standardized
|
||||
// format.
|
||||
warningPrinter *printers.WarningPrinter
|
||||
}
|
||||
|
||||
// ToRESTConfig implements RESTClientGetter.
|
||||
// Returns a REST client configuration based on a provided path
|
||||
// to a .kubeconfig file, loading rules, and config flag overrides.
|
||||
// Expects the AddFlags method to have been called. If WrapConfigFn
|
||||
// is non-nil this function can transform config before return.
|
||||
func (f *ConfigFlags) ToRESTConfig() (*rest.Config, error) {
|
||||
c, err := f.ToRawKubeConfigLoader().ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f.WrapConfigFn != nil {
|
||||
return f.WrapConfigFn(c), nil
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ToRawKubeConfigLoader binds config flag values to config overrides
|
||||
// Returns an interactive clientConfig if the password flag is enabled,
|
||||
// or a non-interactive clientConfig otherwise.
|
||||
func (f *ConfigFlags) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
if f.usePersistentConfig {
|
||||
return f.toRawKubePersistentConfigLoader()
|
||||
}
|
||||
return f.toRawKubeConfigLoader()
|
||||
}
|
||||
|
||||
func (f *ConfigFlags) toRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
// use the standard defaults for this client command
|
||||
// DEPRECATED: remove and replace with something more accurate
|
||||
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
|
||||
|
||||
if f.KubeConfig != nil {
|
||||
loadingRules.ExplicitPath = *f.KubeConfig
|
||||
}
|
||||
|
||||
overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults}
|
||||
|
||||
// bind auth info flag values to overrides
|
||||
if f.CertFile != nil {
|
||||
overrides.AuthInfo.ClientCertificate = *f.CertFile
|
||||
overrides.AuthInfo.ClientCertificateData = nil
|
||||
}
|
||||
if f.KeyFile != nil {
|
||||
overrides.AuthInfo.ClientKey = *f.KeyFile
|
||||
overrides.AuthInfo.ClientKeyData = nil
|
||||
}
|
||||
if f.BearerToken != nil {
|
||||
overrides.AuthInfo.Token = *f.BearerToken
|
||||
overrides.AuthInfo.TokenFile = ""
|
||||
}
|
||||
if f.Impersonate != nil {
|
||||
overrides.AuthInfo.Impersonate = *f.Impersonate
|
||||
}
|
||||
if f.ImpersonateUserExtra != nil && len(*f.ImpersonateUserExtra) > 0 {
|
||||
userExtras := make(map[string][]string)
|
||||
for _, extra := range *f.ImpersonateUserExtra {
|
||||
parts := strings.SplitN(extra, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := parts[0]
|
||||
value := parts[1]
|
||||
userExtras[key] = append(userExtras[key], value)
|
||||
}
|
||||
overrides.AuthInfo.ImpersonateUserExtra = userExtras
|
||||
}
|
||||
if f.ImpersonateUID != nil {
|
||||
overrides.AuthInfo.ImpersonateUID = *f.ImpersonateUID
|
||||
}
|
||||
if f.ImpersonateGroup != nil {
|
||||
overrides.AuthInfo.ImpersonateGroups = *f.ImpersonateGroup
|
||||
}
|
||||
if f.Username != nil {
|
||||
overrides.AuthInfo.Username = *f.Username
|
||||
}
|
||||
if f.Password != nil {
|
||||
overrides.AuthInfo.Password = *f.Password
|
||||
}
|
||||
|
||||
// bind cluster flags
|
||||
if f.APIServer != nil {
|
||||
overrides.ClusterInfo.Server = *f.APIServer
|
||||
}
|
||||
if f.TLSServerName != nil {
|
||||
overrides.ClusterInfo.TLSServerName = *f.TLSServerName
|
||||
}
|
||||
if f.CAFile != nil {
|
||||
overrides.ClusterInfo.CertificateAuthority = *f.CAFile
|
||||
}
|
||||
if f.Insecure != nil {
|
||||
overrides.ClusterInfo.InsecureSkipTLSVerify = *f.Insecure
|
||||
}
|
||||
if f.DisableCompression != nil {
|
||||
overrides.ClusterInfo.DisableCompression = *f.DisableCompression
|
||||
}
|
||||
|
||||
// bind context flags
|
||||
if f.Context != nil {
|
||||
overrides.CurrentContext = *f.Context
|
||||
}
|
||||
if f.ClusterName != nil {
|
||||
overrides.Context.Cluster = *f.ClusterName
|
||||
}
|
||||
if f.AuthInfoName != nil {
|
||||
overrides.Context.AuthInfo = *f.AuthInfoName
|
||||
}
|
||||
if f.Namespace != nil {
|
||||
overrides.Context.Namespace = *f.Namespace
|
||||
}
|
||||
|
||||
if f.Timeout != nil {
|
||||
overrides.Timeout = *f.Timeout
|
||||
}
|
||||
|
||||
// we only have an interactive prompt when a password is allowed
|
||||
if f.Password == nil {
|
||||
return &clientConfig{clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)}
|
||||
}
|
||||
return &clientConfig{clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin)}
|
||||
}
|
||||
|
||||
// toRawKubePersistentConfigLoader binds config flag values to config overrides
|
||||
// Returns a persistent clientConfig for propagation.
|
||||
func (f *ConfigFlags) toRawKubePersistentConfigLoader() clientcmd.ClientConfig {
|
||||
f.clientConfigLock.Lock()
|
||||
defer f.clientConfigLock.Unlock()
|
||||
|
||||
if f.clientConfig == nil {
|
||||
f.clientConfig = f.toRawKubeConfigLoader()
|
||||
}
|
||||
|
||||
return f.clientConfig
|
||||
}
|
||||
|
||||
// ToDiscoveryClient implements RESTClientGetter.
|
||||
// Expects the AddFlags method to have been called.
|
||||
// Returns a CachedDiscoveryInterface using a computed RESTConfig.
|
||||
func (f *ConfigFlags) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
if f.usePersistentConfig {
|
||||
return f.toPersistentDiscoveryClient()
|
||||
}
|
||||
return f.toDiscoveryClient()
|
||||
}
|
||||
|
||||
func (f *ConfigFlags) toPersistentDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
f.discoveryClientLock.Lock()
|
||||
defer f.discoveryClientLock.Unlock()
|
||||
|
||||
if f.discoveryClient == nil {
|
||||
discoveryClient, err := f.toDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.discoveryClient = discoveryClient
|
||||
}
|
||||
return f.discoveryClient, nil
|
||||
}
|
||||
|
||||
func (f *ConfigFlags) toDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
config, err := f.ToRESTConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Burst = f.discoveryBurst
|
||||
config.QPS = f.discoveryQPS
|
||||
|
||||
cacheDir := getDefaultCacheDir()
|
||||
|
||||
// retrieve a user-provided value for the "cache-dir"
|
||||
// override httpCacheDir and discoveryCacheDir if user-value is given.
|
||||
// user-provided value has higher precedence than default
|
||||
// and KUBECACHEDIR environment variable.
|
||||
if f.CacheDir != nil && *f.CacheDir != "" && *f.CacheDir != getDefaultCacheDir() {
|
||||
cacheDir = *f.CacheDir
|
||||
}
|
||||
|
||||
httpCacheDir := filepath.Join(cacheDir, "http")
|
||||
discoveryCacheDir := computeDiscoverCacheDir(filepath.Join(cacheDir, "discovery"), config.Host)
|
||||
|
||||
return diskcached.NewCachedDiscoveryClientForConfig(config, discoveryCacheDir, httpCacheDir, time.Duration(6*time.Hour))
|
||||
}
|
||||
|
||||
// getDefaultCacheDir returns default caching directory path.
|
||||
// it first looks at KUBECACHEDIR env var if it is set, otherwise
|
||||
// it returns standard kube cache dir.
|
||||
func getDefaultCacheDir() string {
|
||||
if kcd := os.Getenv("KUBECACHEDIR"); kcd != "" {
|
||||
return kcd
|
||||
}
|
||||
|
||||
return filepath.Join(homedir.HomeDir(), ".kube", "cache")
|
||||
}
|
||||
|
||||
// ToRESTMapper returns a mapper.
|
||||
func (f *ConfigFlags) ToRESTMapper() (meta.RESTMapper, error) {
|
||||
if f.usePersistentConfig {
|
||||
return f.toPersistentRESTMapper()
|
||||
}
|
||||
return f.toRESTMapper()
|
||||
}
|
||||
|
||||
func (f *ConfigFlags) toPersistentRESTMapper() (meta.RESTMapper, error) {
|
||||
f.restMapperLock.Lock()
|
||||
defer f.restMapperLock.Unlock()
|
||||
|
||||
if f.restMapper == nil {
|
||||
restMapper, err := f.toRESTMapper()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.restMapper = restMapper
|
||||
}
|
||||
return f.restMapper, nil
|
||||
}
|
||||
|
||||
func (f *ConfigFlags) toRESTMapper() (meta.RESTMapper, error) {
|
||||
discoveryClient, err := f.ToDiscoveryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
|
||||
expander := restmapper.NewShortcutExpander(mapper, discoveryClient, func(a string) {
|
||||
if f.warningPrinter != nil {
|
||||
f.warningPrinter.Print(a)
|
||||
}
|
||||
})
|
||||
return expander, nil
|
||||
}
|
||||
|
||||
// AddFlags binds client configuration flags to a given flagset
|
||||
func (f *ConfigFlags) AddFlags(flags *pflag.FlagSet) {
|
||||
if f.KubeConfig != nil {
|
||||
flags.StringVar(f.KubeConfig, "kubeconfig", *f.KubeConfig, "Path to the kubeconfig file to use for CLI requests.")
|
||||
}
|
||||
if f.CacheDir != nil {
|
||||
flags.StringVar(f.CacheDir, flagCacheDir, *f.CacheDir, "Default cache directory")
|
||||
}
|
||||
|
||||
// add config options
|
||||
if f.CertFile != nil {
|
||||
flags.StringVar(f.CertFile, flagCertFile, *f.CertFile, "Path to a client certificate file for TLS")
|
||||
}
|
||||
if f.KeyFile != nil {
|
||||
flags.StringVar(f.KeyFile, flagKeyFile, *f.KeyFile, "Path to a client key file for TLS")
|
||||
}
|
||||
if f.BearerToken != nil {
|
||||
flags.StringVar(f.BearerToken, flagBearerToken, *f.BearerToken, "Bearer token for authentication to the API server")
|
||||
}
|
||||
if f.Impersonate != nil {
|
||||
flags.StringVar(f.Impersonate, flagImpersonate, *f.Impersonate, "Username to impersonate for the operation. User could be a regular user or a service account in a namespace.")
|
||||
}
|
||||
if f.ImpersonateUID != nil {
|
||||
flags.StringVar(f.ImpersonateUID, flagImpersonateUID, *f.ImpersonateUID, "UID to impersonate for the operation.")
|
||||
}
|
||||
if f.ImpersonateGroup != nil {
|
||||
flags.StringArrayVar(f.ImpersonateGroup, flagImpersonateGroup, *f.ImpersonateGroup, "Group to impersonate for the operation, this flag can be repeated to specify multiple groups.")
|
||||
}
|
||||
if f.ImpersonateUserExtra != nil {
|
||||
flags.StringArrayVar(f.ImpersonateUserExtra, flagImpersonateUserExtra, *f.ImpersonateUserExtra, "User extras to impersonate for the operation, this flag can be repeated to specify multiple values for the same key.")
|
||||
}
|
||||
if f.Username != nil {
|
||||
flags.StringVar(f.Username, flagUsername, *f.Username, "Username for basic authentication to the API server")
|
||||
}
|
||||
if f.Password != nil {
|
||||
flags.StringVar(f.Password, flagPassword, *f.Password, "Password for basic authentication to the API server")
|
||||
}
|
||||
if f.ClusterName != nil {
|
||||
flags.StringVar(f.ClusterName, flagClusterName, *f.ClusterName, "The name of the kubeconfig cluster to use")
|
||||
}
|
||||
if f.AuthInfoName != nil {
|
||||
flags.StringVar(f.AuthInfoName, flagAuthInfoName, *f.AuthInfoName, "The name of the kubeconfig user to use")
|
||||
}
|
||||
if f.Namespace != nil {
|
||||
flags.StringVarP(f.Namespace, flagNamespace, "n", *f.Namespace, "If present, the namespace scope for this CLI request")
|
||||
}
|
||||
if f.Context != nil {
|
||||
flags.StringVar(f.Context, flagContext, *f.Context, "The name of the kubeconfig context to use")
|
||||
}
|
||||
|
||||
if f.APIServer != nil {
|
||||
flags.StringVarP(f.APIServer, flagAPIServer, "s", *f.APIServer, "The address and port of the Kubernetes API server")
|
||||
}
|
||||
if f.TLSServerName != nil {
|
||||
flags.StringVar(f.TLSServerName, flagTLSServerName, *f.TLSServerName, "Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used")
|
||||
}
|
||||
if f.Insecure != nil {
|
||||
flags.BoolVar(f.Insecure, flagInsecure, *f.Insecure, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
|
||||
}
|
||||
if f.CAFile != nil {
|
||||
flags.StringVar(f.CAFile, flagCAFile, *f.CAFile, "Path to a cert file for the certificate authority")
|
||||
}
|
||||
if f.Timeout != nil {
|
||||
flags.StringVar(f.Timeout, flagTimeout, *f.Timeout, "The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests.")
|
||||
}
|
||||
if f.DisableCompression != nil {
|
||||
flags.BoolVar(f.DisableCompression, flagDisableCompression, *f.DisableCompression, "If true, opt-out of response compression for all requests to the server")
|
||||
}
|
||||
}
|
||||
|
||||
// WithDeprecatedPasswordFlag enables the username and password config flags
|
||||
func (f *ConfigFlags) WithDeprecatedPasswordFlag() *ConfigFlags {
|
||||
f.Username = ptr.To("")
|
||||
f.Password = ptr.To("")
|
||||
return f
|
||||
}
|
||||
|
||||
// WithDiscoveryBurst sets the RESTClient burst for discovery.
|
||||
func (f *ConfigFlags) WithDiscoveryBurst(discoveryBurst int) *ConfigFlags {
|
||||
f.discoveryBurst = discoveryBurst
|
||||
return f
|
||||
}
|
||||
|
||||
// WithDiscoveryQPS sets the RESTClient QPS for discovery.
|
||||
func (f *ConfigFlags) WithDiscoveryQPS(discoveryQPS float32) *ConfigFlags {
|
||||
f.discoveryQPS = discoveryQPS
|
||||
return f
|
||||
}
|
||||
|
||||
// WithWrapConfigFn allows providing a wrapper function for the client Config.
|
||||
func (f *ConfigFlags) WithWrapConfigFn(wrapConfigFn func(*rest.Config) *rest.Config) *ConfigFlags {
|
||||
f.WrapConfigFn = wrapConfigFn
|
||||
return f
|
||||
}
|
||||
|
||||
// WithWarningPrinter initializes WarningPrinter with the given IOStreams
|
||||
func (f *ConfigFlags) WithWarningPrinter(ioStreams genericiooptions.IOStreams) *ConfigFlags {
|
||||
f.warningPrinter = printers.NewWarningPrinter(ioStreams.ErrOut, printers.WarningPrinterOptions{Color: printers.AllowsColorOutput(ioStreams.ErrOut)})
|
||||
return f
|
||||
}
|
||||
|
||||
// NewConfigFlags returns ConfigFlags with default values set
|
||||
func NewConfigFlags(usePersistentConfig bool) *ConfigFlags {
|
||||
impersonateGroup := []string{}
|
||||
impersonateUserExtra := []string{}
|
||||
insecure := false
|
||||
disableCompression := false
|
||||
|
||||
return &ConfigFlags{
|
||||
Insecure: &insecure,
|
||||
Timeout: ptr.To("0"),
|
||||
KubeConfig: ptr.To(""),
|
||||
|
||||
CacheDir: ptr.To(getDefaultCacheDir()),
|
||||
ClusterName: ptr.To(""),
|
||||
AuthInfoName: ptr.To(""),
|
||||
Context: ptr.To(""),
|
||||
Namespace: ptr.To(""),
|
||||
APIServer: ptr.To(""),
|
||||
TLSServerName: ptr.To(""),
|
||||
CertFile: ptr.To(""),
|
||||
KeyFile: ptr.To(""),
|
||||
CAFile: ptr.To(""),
|
||||
BearerToken: ptr.To(""),
|
||||
Impersonate: ptr.To(""),
|
||||
ImpersonateUID: ptr.To(""),
|
||||
ImpersonateGroup: &impersonateGroup,
|
||||
ImpersonateUserExtra: &impersonateUserExtra,
|
||||
DisableCompression: &disableCompression,
|
||||
|
||||
usePersistentConfig: usePersistentConfig,
|
||||
// The more groups you have, the more discovery requests you need to make.
|
||||
// with a burst of 300, we will not be rate-limiting for most clusters but
|
||||
// the safeguard will still be here. This config is only used for discovery.
|
||||
discoveryBurst: 300,
|
||||
}
|
||||
}
|
||||
|
||||
// overlyCautiousIllegalFileCharacters matches characters that *might* not be supported. Windows is really restrictive, so this is really restrictive
|
||||
var overlyCautiousIllegalFileCharacters = regexp.MustCompile(`[^(\w/.)]`)
|
||||
|
||||
// computeDiscoverCacheDir takes the parentDir and the host and comes up with a "usually non-colliding" name.
|
||||
func computeDiscoverCacheDir(parentDir, host string) string {
|
||||
// strip the optional scheme from host if its there:
|
||||
schemelessHost := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1)
|
||||
// now do a simple collapse of non-AZ09 characters. Collisions are possible but unlikely. Even if we do collide the problem is short lived
|
||||
safeHost := overlyCautiousIllegalFileCharacters.ReplaceAllString(schemelessHost, "_")
|
||||
return filepath.Join(parentDir, safeHost)
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
// TestConfigFlags contains clientConfig struct
|
||||
// and interfaces that implements RESTClientGetter
|
||||
type TestConfigFlags struct {
|
||||
clientConfig clientcmd.ClientConfig
|
||||
discoveryClient discovery.CachedDiscoveryInterface
|
||||
restMapper meta.RESTMapper
|
||||
}
|
||||
|
||||
// ToRawKubeConfigLoader implements RESTClientGetter
|
||||
// Returns a clientconfig if it's set
|
||||
func (f *TestConfigFlags) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
if f.clientConfig == nil {
|
||||
panic("attempt to obtain a test RawKubeConfigLoader with no clientConfig specified")
|
||||
}
|
||||
return f.clientConfig
|
||||
}
|
||||
|
||||
// ToRESTConfig implements RESTClientGetter.
|
||||
// Returns a REST client configuration based on a provided path
|
||||
// to a .kubeconfig file, loading rules, and config flag overrides.
|
||||
// Expects the AddFlags method to have been called.
|
||||
func (f *TestConfigFlags) ToRESTConfig() (*rest.Config, error) {
|
||||
return f.ToRawKubeConfigLoader().ClientConfig()
|
||||
}
|
||||
|
||||
// ToDiscoveryClient implements RESTClientGetter.
|
||||
// Returns a CachedDiscoveryInterface
|
||||
func (f *TestConfigFlags) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
return f.discoveryClient, nil
|
||||
}
|
||||
|
||||
// ToRESTMapper implements RESTClientGetter.
|
||||
// Returns a mapper.
|
||||
func (f *TestConfigFlags) ToRESTMapper() (meta.RESTMapper, error) {
|
||||
if f.restMapper != nil {
|
||||
return f.restMapper, nil
|
||||
}
|
||||
if f.discoveryClient != nil {
|
||||
mapper := restmapper.NewDeferredDiscoveryRESTMapper(f.discoveryClient)
|
||||
expander := restmapper.NewShortcutExpander(mapper, f.discoveryClient, nil)
|
||||
return expander, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no restmapper")
|
||||
}
|
||||
|
||||
// WithClientConfig sets the clientConfig flag
|
||||
func (f *TestConfigFlags) WithClientConfig(clientConfig clientcmd.ClientConfig) *TestConfigFlags {
|
||||
f.clientConfig = clientConfig
|
||||
return f
|
||||
}
|
||||
|
||||
// WithRESTMapper sets the restMapper flag
|
||||
func (f *TestConfigFlags) WithRESTMapper(mapper meta.RESTMapper) *TestConfigFlags {
|
||||
f.restMapper = mapper
|
||||
return f
|
||||
}
|
||||
|
||||
// WithDiscoveryClient sets the discoveryClient flag
|
||||
func (f *TestConfigFlags) WithDiscoveryClient(c discovery.CachedDiscoveryInterface) *TestConfigFlags {
|
||||
f.discoveryClient = c
|
||||
return f
|
||||
}
|
||||
|
||||
// WithNamespace sets the clientConfig flag by modifying delagate and namespace
|
||||
func (f *TestConfigFlags) WithNamespace(ns string) *TestConfigFlags {
|
||||
if f.clientConfig == nil {
|
||||
panic("attempt to obtain a test RawKubeConfigLoader with no clientConfig specified")
|
||||
}
|
||||
f.clientConfig = &namespacedClientConfig{
|
||||
delegate: f.clientConfig,
|
||||
namespace: ns,
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// NewTestConfigFlags builds a TestConfigFlags struct to test ConfigFlags
|
||||
func NewTestConfigFlags() *TestConfigFlags {
|
||||
return &TestConfigFlags{}
|
||||
}
|
||||
|
||||
type namespacedClientConfig struct {
|
||||
delegate clientcmd.ClientConfig
|
||||
namespace string
|
||||
}
|
||||
|
||||
func (c *namespacedClientConfig) Namespace() (string, bool, error) {
|
||||
return c.namespace, len(c.namespace) > 0, nil
|
||||
}
|
||||
|
||||
func (c *namespacedClientConfig) RawConfig() (clientcmdapi.Config, error) {
|
||||
return c.delegate.RawConfig()
|
||||
}
|
||||
func (c *namespacedClientConfig) ClientConfig() (*rest.Config, error) {
|
||||
return c.delegate.ClientConfig()
|
||||
}
|
||||
func (c *namespacedClientConfig) ConfigAccess() clientcmd.ConfigAccess {
|
||||
return c.delegate.ConfigAccess()
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions contains flags which can be added to your command, bound, completed, and produce
|
||||
// useful helper functions. Nothing in this package can depend on kube/kube
|
||||
package genericclioptions
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
)
|
||||
|
||||
// FileNameFlags are flags for processing files.
|
||||
// Usage of this struct by itself is discouraged.
|
||||
// These flags are composed by ResourceBuilderFlags
|
||||
// which should be used instead.
|
||||
type FileNameFlags struct {
|
||||
Usage string
|
||||
|
||||
Filenames *[]string
|
||||
Kustomize *string
|
||||
Recursive *bool
|
||||
}
|
||||
|
||||
// ToOptions creates a new FileNameOptions struct and sets FilenameOptions based on FileNameflags
|
||||
func (o *FileNameFlags) ToOptions() resource.FilenameOptions {
|
||||
options := resource.FilenameOptions{}
|
||||
|
||||
if o == nil {
|
||||
return options
|
||||
}
|
||||
|
||||
if o.Recursive != nil {
|
||||
options.Recursive = *o.Recursive
|
||||
}
|
||||
if o.Filenames != nil {
|
||||
options.Filenames = *o.Filenames
|
||||
}
|
||||
if o.Kustomize != nil {
|
||||
options.Kustomize = *o.Kustomize
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// AddFlags binds file name flags to a given flagset
|
||||
func (o *FileNameFlags) AddFlags(flags *pflag.FlagSet) {
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if o.Recursive != nil {
|
||||
flags.BoolVarP(o.Recursive, "recursive", "R", *o.Recursive, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.")
|
||||
}
|
||||
if o.Filenames != nil {
|
||||
flags.StringSliceVarP(o.Filenames, "filename", "f", *o.Filenames, o.Usage)
|
||||
annotations := make([]string, 0, len(resource.FileExtensions))
|
||||
for _, ext := range resource.FileExtensions {
|
||||
annotations = append(annotations, strings.TrimLeft(ext, "."))
|
||||
}
|
||||
flags.SetAnnotation("filename", cobra.BashCompFilenameExt, annotations)
|
||||
}
|
||||
if o.Kustomize != nil {
|
||||
flags.StringVarP(o.Kustomize, "kustomize", "k", *o.Kustomize,
|
||||
"Process a kustomization directory. This flag can't be used together with -f or -R.")
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericiooptions"
|
||||
)
|
||||
|
||||
// IOStreams provides the standard names for iostreams. This is useful for embedding and for unit testing.
|
||||
// Inconsistent and different names make it hard to read and review code
|
||||
// DEPRECATED: use genericiooptions.IOStreams
|
||||
type IOStreams = genericiooptions.IOStreams
|
||||
|
||||
// NewTestIOStreams returns a valid IOStreams and in, out, errout buffers for unit tests
|
||||
// DEPRECATED: use genericiooptions.NewTestIOStreams
|
||||
func NewTestIOStreams() (genericiooptions.IOStreams, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
|
||||
in := &bytes.Buffer{}
|
||||
out := &bytes.Buffer{}
|
||||
errOut := &bytes.Buffer{}
|
||||
|
||||
return IOStreams{
|
||||
In: in,
|
||||
Out: out,
|
||||
ErrOut: errOut,
|
||||
}, in, out, errOut
|
||||
}
|
||||
|
||||
// NewTestIOStreamsDiscard returns a valid IOStreams that just discards
|
||||
// DEPRECATED: use genericiooptions.NewTestIOStreamsDiscard
|
||||
func NewTestIOStreamsDiscard() genericiooptions.IOStreams {
|
||||
in := &bytes.Buffer{}
|
||||
return IOStreams{
|
||||
In: in,
|
||||
Out: io.Discard,
|
||||
ErrOut: io.Discard,
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
// AllowedFormats returns slice of string of allowed JSONYaml printing format
|
||||
func (f *JSONYamlPrintFlags) AllowedFormats() []string {
|
||||
if f == nil {
|
||||
return []string{}
|
||||
}
|
||||
formats := []string{"json", "yaml"}
|
||||
// We can't use the cmdutil pkg directly because of import cycle.
|
||||
if strings.ToLower(os.Getenv("KUBECTL_KYAML")) != "false" {
|
||||
formats = append(formats, "kyaml")
|
||||
}
|
||||
return formats
|
||||
}
|
||||
|
||||
// JSONYamlPrintFlags provides default flags necessary for json/yaml printing.
|
||||
// Given the following flag values, a printer can be requested that knows
|
||||
// how to handle printing based on these values.
|
||||
type JSONYamlPrintFlags struct {
|
||||
ShowManagedFields bool
|
||||
}
|
||||
|
||||
// ToPrinter receives an outputFormat and returns a printer capable of
|
||||
// handling --output=(yaml|json) printing.
|
||||
// Returns false if the specified outputFormat does not match a supported format.
|
||||
// Supported Format types can be found in pkg/printers/printers.go
|
||||
func (f *JSONYamlPrintFlags) ToPrinter(outputFormat string) (printers.ResourcePrinter, error) {
|
||||
var printer printers.ResourcePrinter
|
||||
|
||||
outputFormat = strings.ToLower(outputFormat)
|
||||
|
||||
valid := f.AllowedFormats()
|
||||
if !slices.Contains(valid, outputFormat) {
|
||||
return nil, NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: valid}
|
||||
}
|
||||
|
||||
switch outputFormat {
|
||||
case "json":
|
||||
printer = &printers.JSONPrinter{}
|
||||
case "yaml":
|
||||
printer = &printers.YAMLPrinter{}
|
||||
case "kyaml":
|
||||
printer = &printers.KYAMLPrinter{}
|
||||
default:
|
||||
return nil, NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: f.AllowedFormats()}
|
||||
}
|
||||
|
||||
if !f.ShowManagedFields {
|
||||
printer = &printers.OmitManagedFieldsPrinter{Delegate: printer}
|
||||
}
|
||||
return printer, nil
|
||||
}
|
||||
|
||||
// AddFlags receives a *cobra.Command reference and binds
|
||||
// flags related to JSON or Yaml printing to it
|
||||
func (f *JSONYamlPrintFlags) AddFlags(c *cobra.Command) {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Flags().BoolVar(&f.ShowManagedFields, "show-managed-fields", f.ShowManagedFields, "If true, keep the managedFields when printing objects in JSON or YAML format.")
|
||||
}
|
||||
|
||||
// NewJSONYamlPrintFlags returns flags associated with
|
||||
// yaml or json printing, with default values set.
|
||||
func NewJSONYamlPrintFlags() *JSONYamlPrintFlags {
|
||||
return &JSONYamlPrintFlags{}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
// templates are logically optional for specifying a format.
|
||||
// this allows a user to specify a template format value
|
||||
// as --output=jsonpath=
|
||||
var jsonFormats = map[string]bool{
|
||||
"jsonpath": true,
|
||||
"jsonpath-file": true,
|
||||
"jsonpath-as-json": true,
|
||||
}
|
||||
|
||||
// JSONPathPrintFlags provides default flags necessary for template printing.
|
||||
// Given the following flag values, a printer can be requested that knows
|
||||
// how to handle printing based on these values.
|
||||
type JSONPathPrintFlags struct {
|
||||
// indicates if it is OK to ignore missing keys for rendering
|
||||
// an output template.
|
||||
AllowMissingKeys *bool
|
||||
TemplateArgument *string
|
||||
}
|
||||
|
||||
// AllowedFormats returns slice of string of allowed JSONPath printing format
|
||||
func (f *JSONPathPrintFlags) AllowedFormats() []string {
|
||||
formats := make([]string, 0, len(jsonFormats))
|
||||
for format := range jsonFormats {
|
||||
formats = append(formats, format)
|
||||
}
|
||||
sort.Strings(formats)
|
||||
return formats
|
||||
}
|
||||
|
||||
// ToPrinter receives an templateFormat and returns a printer capable of
|
||||
// handling --template format printing.
|
||||
// Returns false if the specified templateFormat does not match a template format.
|
||||
func (f *JSONPathPrintFlags) ToPrinter(templateFormat string) (printers.ResourcePrinter, error) {
|
||||
if (f.TemplateArgument == nil || len(*f.TemplateArgument) == 0) && len(templateFormat) == 0 {
|
||||
return nil, NoCompatiblePrinterError{Options: f, OutputFormat: &templateFormat}
|
||||
}
|
||||
|
||||
templateValue := ""
|
||||
|
||||
if f.TemplateArgument == nil || len(*f.TemplateArgument) == 0 {
|
||||
for format := range jsonFormats {
|
||||
format = format + "="
|
||||
if strings.HasPrefix(templateFormat, format) {
|
||||
templateValue = templateFormat[len(format):]
|
||||
templateFormat = format[:len(format)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
templateValue = *f.TemplateArgument
|
||||
}
|
||||
|
||||
if _, supportedFormat := jsonFormats[templateFormat]; !supportedFormat {
|
||||
return nil, NoCompatiblePrinterError{OutputFormat: &templateFormat, AllowedFormats: f.AllowedFormats()}
|
||||
}
|
||||
|
||||
if len(templateValue) == 0 {
|
||||
return nil, fmt.Errorf("template format specified but no template given")
|
||||
}
|
||||
|
||||
if templateFormat == "jsonpath-file" {
|
||||
data, err := os.ReadFile(templateValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading --template %s, %v", templateValue, err)
|
||||
}
|
||||
|
||||
templateValue = string(data)
|
||||
}
|
||||
|
||||
p, err := printers.NewJSONPathPrinter(templateValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing jsonpath %s, %v", templateValue, err)
|
||||
}
|
||||
|
||||
allowMissingKeys := true
|
||||
if f.AllowMissingKeys != nil {
|
||||
allowMissingKeys = *f.AllowMissingKeys
|
||||
}
|
||||
|
||||
p.AllowMissingKeys(allowMissingKeys)
|
||||
|
||||
if templateFormat == "jsonpath-as-json" {
|
||||
p.EnableJSONOutput(true)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// AddFlags receives a *cobra.Command reference and binds
|
||||
// flags related to template printing to it
|
||||
func (f *JSONPathPrintFlags) AddFlags(c *cobra.Command) {
|
||||
if f.TemplateArgument != nil {
|
||||
c.Flags().StringVar(f.TemplateArgument, "template", *f.TemplateArgument, "Template string or path to template file to use when --output=jsonpath, --output=jsonpath-file.")
|
||||
c.MarkFlagFilename("template")
|
||||
}
|
||||
if f.AllowMissingKeys != nil {
|
||||
c.Flags().BoolVar(f.AllowMissingKeys, "allow-missing-template-keys", *f.AllowMissingKeys, "If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats.")
|
||||
}
|
||||
}
|
||||
|
||||
// NewJSONPathPrintFlags returns flags associated with
|
||||
// --template printing, with default values set.
|
||||
func NewJSONPathPrintFlags(templateValue string, allowMissingKeys bool) *JSONPathPrintFlags {
|
||||
return &JSONPathPrintFlags{
|
||||
TemplateArgument: &templateValue,
|
||||
AllowMissingKeys: &allowMissingKeys,
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
// KubeTemplatePrintFlags composes print flags that provide both a JSONPath and a go-template printer.
|
||||
// This is necessary if dealing with cases that require support both both printers, since both sets of flags
|
||||
// require overlapping flags.
|
||||
type KubeTemplatePrintFlags struct {
|
||||
GoTemplatePrintFlags *GoTemplatePrintFlags
|
||||
JSONPathPrintFlags *JSONPathPrintFlags
|
||||
|
||||
AllowMissingKeys *bool
|
||||
TemplateArgument *string
|
||||
}
|
||||
|
||||
// AllowedFormats returns slice of string of allowed GoTemplete and JSONPathPrint printing formats
|
||||
func (f *KubeTemplatePrintFlags) AllowedFormats() []string {
|
||||
if f == nil {
|
||||
return []string{}
|
||||
}
|
||||
return append(f.GoTemplatePrintFlags.AllowedFormats(), f.JSONPathPrintFlags.AllowedFormats()...)
|
||||
}
|
||||
|
||||
// ToPrinter receives an outputFormat and returns a printer capable of
|
||||
// handling --template printing.
|
||||
// Returns false if the specified outputFormat does not match a supported format.
|
||||
// Supported Format types can be found in pkg/printers/printers.go
|
||||
func (f *KubeTemplatePrintFlags) ToPrinter(outputFormat string) (printers.ResourcePrinter, error) {
|
||||
if f == nil {
|
||||
return nil, NoCompatiblePrinterError{}
|
||||
}
|
||||
|
||||
if p, err := f.JSONPathPrintFlags.ToPrinter(outputFormat); !IsNoCompatiblePrinterError(err) {
|
||||
return p, err
|
||||
}
|
||||
return f.GoTemplatePrintFlags.ToPrinter(outputFormat)
|
||||
}
|
||||
|
||||
// AddFlags receives a *cobra.Command reference and binds
|
||||
// flags related to template printing to it
|
||||
func (f *KubeTemplatePrintFlags) AddFlags(c *cobra.Command) {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if f.TemplateArgument != nil {
|
||||
c.Flags().StringVar(f.TemplateArgument, "template", *f.TemplateArgument, "Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].")
|
||||
c.MarkFlagFilename("template")
|
||||
}
|
||||
if f.AllowMissingKeys != nil {
|
||||
c.Flags().BoolVar(f.AllowMissingKeys, "allow-missing-template-keys", *f.AllowMissingKeys, "If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats.")
|
||||
}
|
||||
}
|
||||
|
||||
// NewKubeTemplatePrintFlags returns flags associated with
|
||||
// --template printing, with default values set.
|
||||
func NewKubeTemplatePrintFlags() *KubeTemplatePrintFlags {
|
||||
allowMissingKeysPtr := true
|
||||
templateArgPtr := ""
|
||||
|
||||
return &KubeTemplatePrintFlags{
|
||||
GoTemplatePrintFlags: &GoTemplatePrintFlags{
|
||||
TemplateArgument: &templateArgPtr,
|
||||
AllowMissingKeys: &allowMissingKeysPtr,
|
||||
},
|
||||
JSONPathPrintFlags: &JSONPathPrintFlags{
|
||||
TemplateArgument: &templateArgPtr,
|
||||
AllowMissingKeys: &allowMissingKeysPtr,
|
||||
},
|
||||
|
||||
TemplateArgument: &templateArgPtr,
|
||||
AllowMissingKeys: &allowMissingKeysPtr,
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
// NamePrintFlags provides default flags necessary for printing
|
||||
// a resource's fully-qualified Kind.group/name, or a successful
|
||||
// message about that resource if an Operation is provided.
|
||||
type NamePrintFlags struct {
|
||||
// Operation describes the name of the action that
|
||||
// took place on an object, to be included in the
|
||||
// finalized "successful" message.
|
||||
Operation string
|
||||
}
|
||||
|
||||
// Complete sets NamePrintFlags operation flag from successTemplate
|
||||
func (f *NamePrintFlags) Complete(successTemplate string) error {
|
||||
f.Operation = fmt.Sprintf(successTemplate, f.Operation)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllowedFormats returns slice of string of allowed Name printing format
|
||||
func (f *NamePrintFlags) AllowedFormats() []string {
|
||||
if f == nil {
|
||||
return []string{}
|
||||
}
|
||||
return []string{"name"}
|
||||
}
|
||||
|
||||
// ToPrinter receives an outputFormat and returns a printer capable of
|
||||
// handling --output=name printing.
|
||||
// Returns false if the specified outputFormat does not match a supported format.
|
||||
// Supported format types can be found in pkg/printers/printers.go
|
||||
func (f *NamePrintFlags) ToPrinter(outputFormat string) (printers.ResourcePrinter, error) {
|
||||
namePrinter := &printers.NamePrinter{
|
||||
Operation: f.Operation,
|
||||
}
|
||||
|
||||
outputFormat = strings.ToLower(outputFormat)
|
||||
switch outputFormat {
|
||||
case "name":
|
||||
namePrinter.ShortOutput = true
|
||||
fallthrough
|
||||
case "":
|
||||
return namePrinter, nil
|
||||
default:
|
||||
return nil, NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: f.AllowedFormats()}
|
||||
}
|
||||
}
|
||||
|
||||
// AddFlags receives a *cobra.Command reference and binds
|
||||
// flags related to name printing to it
|
||||
func (f *NamePrintFlags) AddFlags(c *cobra.Command) {}
|
||||
|
||||
// NewNamePrintFlags returns flags associated with
|
||||
// --name printing, with default values set.
|
||||
func NewNamePrintFlags(operation string) *NamePrintFlags {
|
||||
return &NamePrintFlags{
|
||||
Operation: operation,
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
// NoCompatiblePrinterError is a struct that contains error information.
|
||||
// It will be constructed when a invalid printing format is provided
|
||||
type NoCompatiblePrinterError struct {
|
||||
OutputFormat *string
|
||||
AllowedFormats []string
|
||||
Options interface{}
|
||||
}
|
||||
|
||||
func (e NoCompatiblePrinterError) Error() string {
|
||||
output := ""
|
||||
if e.OutputFormat != nil {
|
||||
output = *e.OutputFormat
|
||||
}
|
||||
|
||||
sort.Strings(e.AllowedFormats)
|
||||
return fmt.Sprintf("unable to match a printer suitable for the output format %q, allowed formats are: %s", output, strings.Join(e.AllowedFormats, ","))
|
||||
}
|
||||
|
||||
// IsNoCompatiblePrinterError returns true if it is a not a compatible printer
|
||||
// otherwise it will return false
|
||||
func IsNoCompatiblePrinterError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok := err.(NoCompatiblePrinterError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// PrintFlags composes common printer flag structs
|
||||
// used across all commands, and provides a method
|
||||
// of retrieving a known printer based on flag values provided.
|
||||
type PrintFlags struct {
|
||||
JSONYamlPrintFlags *JSONYamlPrintFlags
|
||||
NamePrintFlags *NamePrintFlags
|
||||
TemplatePrinterFlags *KubeTemplatePrintFlags
|
||||
|
||||
TypeSetterPrinter *printers.TypeSetterPrinter
|
||||
|
||||
OutputFormat *string
|
||||
|
||||
// OutputFlagSpecified indicates whether the user specifically requested a certain kind of output.
|
||||
// Using this function allows a sophisticated caller to change the flag binding logic if they so desire.
|
||||
OutputFlagSpecified func() bool
|
||||
}
|
||||
|
||||
// Complete sets NamePrintFlags operation flag from successTemplate
|
||||
func (f *PrintFlags) Complete(successTemplate string) error {
|
||||
return f.NamePrintFlags.Complete(successTemplate)
|
||||
}
|
||||
|
||||
// AllowedFormats returns slice of string of allowed JSONYaml/Name/Template printing format
|
||||
func (f *PrintFlags) AllowedFormats() []string {
|
||||
ret := []string{}
|
||||
ret = append(ret, f.JSONYamlPrintFlags.AllowedFormats()...)
|
||||
ret = append(ret, f.NamePrintFlags.AllowedFormats()...)
|
||||
ret = append(ret, f.TemplatePrinterFlags.AllowedFormats()...)
|
||||
return ret
|
||||
}
|
||||
|
||||
// ToPrinter returns a printer capable of
|
||||
// handling --output or --template printing.
|
||||
// Returns false if the specified outputFormat does not match a supported format.
|
||||
// Supported format types can be found in pkg/printers/printers.go
|
||||
func (f *PrintFlags) ToPrinter() (printers.ResourcePrinter, error) {
|
||||
outputFormat := ""
|
||||
if f.OutputFormat != nil {
|
||||
outputFormat = *f.OutputFormat
|
||||
}
|
||||
// For backwards compatibility we want to support a --template argument given, even when no --output format is provided.
|
||||
// If no explicit output format has been provided via the --output flag, fallback
|
||||
// to honoring the --template argument.
|
||||
templateFlagSpecified := f.TemplatePrinterFlags != nil &&
|
||||
f.TemplatePrinterFlags.TemplateArgument != nil &&
|
||||
len(*f.TemplatePrinterFlags.TemplateArgument) > 0
|
||||
outputFlagSpecified := f.OutputFlagSpecified != nil && f.OutputFlagSpecified()
|
||||
if templateFlagSpecified && !outputFlagSpecified {
|
||||
outputFormat = "go-template"
|
||||
}
|
||||
|
||||
if f.JSONYamlPrintFlags != nil {
|
||||
if p, err := f.JSONYamlPrintFlags.ToPrinter(outputFormat); !IsNoCompatiblePrinterError(err) {
|
||||
return f.TypeSetterPrinter.WrapToPrinter(p, err)
|
||||
}
|
||||
}
|
||||
|
||||
if f.NamePrintFlags != nil {
|
||||
if p, err := f.NamePrintFlags.ToPrinter(outputFormat); !IsNoCompatiblePrinterError(err) {
|
||||
return f.TypeSetterPrinter.WrapToPrinter(p, err)
|
||||
}
|
||||
}
|
||||
|
||||
if f.TemplatePrinterFlags != nil {
|
||||
if p, err := f.TemplatePrinterFlags.ToPrinter(outputFormat); !IsNoCompatiblePrinterError(err) {
|
||||
return f.TypeSetterPrinter.WrapToPrinter(p, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, NoCompatiblePrinterError{OutputFormat: f.OutputFormat, AllowedFormats: f.AllowedFormats()}
|
||||
}
|
||||
|
||||
// AddFlags receives a *cobra.Command reference and binds
|
||||
// flags related to JSON/Yaml/Name/Template printing to it
|
||||
func (f *PrintFlags) AddFlags(cmd *cobra.Command) {
|
||||
f.JSONYamlPrintFlags.AddFlags(cmd)
|
||||
f.NamePrintFlags.AddFlags(cmd)
|
||||
f.TemplatePrinterFlags.AddFlags(cmd)
|
||||
|
||||
if f.OutputFormat != nil {
|
||||
cmd.Flags().StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf(`Output format. One of: (%s).`, strings.Join(f.AllowedFormats(), ", ")))
|
||||
if f.OutputFlagSpecified == nil {
|
||||
f.OutputFlagSpecified = func() bool {
|
||||
return cmd.Flag("output").Changed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultOutput sets a default output format if one is not provided through a flag value
|
||||
func (f *PrintFlags) WithDefaultOutput(output string) *PrintFlags {
|
||||
f.OutputFormat = &output
|
||||
return f
|
||||
}
|
||||
|
||||
// WithTypeSetter sets a wrapper than will surround the returned printer with a printer to type resources
|
||||
func (f *PrintFlags) WithTypeSetter(scheme *runtime.Scheme) *PrintFlags {
|
||||
f.TypeSetterPrinter = printers.NewTypeSetter(scheme)
|
||||
return f
|
||||
}
|
||||
|
||||
// NewPrintFlags returns a default *PrintFlags
|
||||
func NewPrintFlags(operation string) *PrintFlags {
|
||||
outputFormat := ""
|
||||
|
||||
return &PrintFlags{
|
||||
OutputFormat: &outputFormat,
|
||||
|
||||
JSONYamlPrintFlags: NewJSONYamlPrintFlags(),
|
||||
NamePrintFlags: NewNamePrintFlags(operation),
|
||||
TemplatePrinterFlags: NewKubeTemplatePrintFlags(),
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
)
|
||||
|
||||
// ChangeCauseAnnotation is the annotation indicating a guess at "why" something was changed
|
||||
const ChangeCauseAnnotation = "kubernetes.io/change-cause"
|
||||
|
||||
// RecordFlags contains all flags associated with the "--record" operation
|
||||
type RecordFlags struct {
|
||||
// Record indicates the state of the recording flag. It is a pointer so a caller can opt out or rebind
|
||||
Record *bool
|
||||
|
||||
changeCause string
|
||||
}
|
||||
|
||||
// ToRecorder returns a ChangeCause recorder if --record=false was not
|
||||
// explicitly given by the user
|
||||
func (f *RecordFlags) ToRecorder() (Recorder, error) {
|
||||
if f == nil {
|
||||
return NoopRecorder{}, nil
|
||||
}
|
||||
|
||||
shouldRecord := false
|
||||
if f.Record != nil {
|
||||
shouldRecord = *f.Record
|
||||
}
|
||||
|
||||
// if flag was explicitly set to false by the user,
|
||||
// do not record
|
||||
if !shouldRecord {
|
||||
return NoopRecorder{}, nil
|
||||
}
|
||||
|
||||
return &ChangeCauseRecorder{
|
||||
changeCause: f.changeCause,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Complete is called before the command is run, but after it is invoked to finish the state of the struct before use.
|
||||
func (f *RecordFlags) Complete(cmd *cobra.Command) error {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
f.changeCause = parseCommandArguments(cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteWithChangeCause alters changeCause value with a new cause
|
||||
func (f *RecordFlags) CompleteWithChangeCause(cause string) error {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
f.changeCause = cause
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddFlags binds the requested flags to the provided flagset
|
||||
// TODO have this only take a flagset
|
||||
func (f *RecordFlags) AddFlags(cmd *cobra.Command) {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if f.Record != nil {
|
||||
cmd.Flags().BoolVar(f.Record, "record", *f.Record, "Record current kubectl command in the resource annotation. If set to false, do not record the command. If set to true, record the command. If not set, default to updating the existing annotation value only if one already exists.")
|
||||
cmd.Flags().MarkDeprecated("record", "--record will be removed in the future")
|
||||
}
|
||||
}
|
||||
|
||||
// NewRecordFlags provides a RecordFlags with reasonable default values set for use
|
||||
func NewRecordFlags() *RecordFlags {
|
||||
record := false
|
||||
|
||||
return &RecordFlags{
|
||||
Record: &record,
|
||||
}
|
||||
}
|
||||
|
||||
// Recorder is used to record why a runtime.Object was changed in an annotation.
|
||||
type Recorder interface {
|
||||
// Record records why a runtime.Object was changed in an annotation.
|
||||
Record(runtime.Object) error
|
||||
MakeRecordMergePatch(runtime.Object) ([]byte, error)
|
||||
}
|
||||
|
||||
// NoopRecorder does nothing. It is a "do nothing" that can be returned so code doesn't switch on it.
|
||||
type NoopRecorder struct{}
|
||||
|
||||
// Record implements Recorder
|
||||
func (r NoopRecorder) Record(obj runtime.Object) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeRecordMergePatch implements Recorder
|
||||
func (r NoopRecorder) MakeRecordMergePatch(obj runtime.Object) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ChangeCauseRecorder annotates a "change-cause" to an input runtime object
|
||||
type ChangeCauseRecorder struct {
|
||||
changeCause string
|
||||
}
|
||||
|
||||
// Record annotates a "change-cause" to a given info if either "shouldRecord" is true,
|
||||
// or the resource info previously contained a "change-cause" annotation.
|
||||
func (r *ChangeCauseRecorder) Record(obj runtime.Object) error {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
annotations := accessor.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
annotations[ChangeCauseAnnotation] = r.changeCause
|
||||
accessor.SetAnnotations(annotations)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeRecordMergePatch produces a merge patch for updating the recording annotation.
|
||||
func (r *ChangeCauseRecorder) MakeRecordMergePatch(obj runtime.Object) ([]byte, error) {
|
||||
// copy so we don't mess with the original
|
||||
objCopy := obj.DeepCopyObject()
|
||||
if err := r.Record(objCopy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oldData, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newData, err := json.Marshal(objCopy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return jsonpatch.CreateMergePatch(oldData, newData)
|
||||
}
|
||||
|
||||
// parseCommandArguments will stringify and return all environment arguments ie. a command run by a client
|
||||
// using the factory.
|
||||
// Set showSecrets false to filter out stuff like secrets.
|
||||
func parseCommandArguments(cmd *cobra.Command) string {
|
||||
if len(os.Args) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
flags := ""
|
||||
parseFunc := func(flag *pflag.Flag, value string) error {
|
||||
flags = flags + " --" + flag.Name
|
||||
if set, ok := flag.Annotations["classified"]; !ok || len(set) == 0 {
|
||||
flags = flags + "=" + value
|
||||
} else {
|
||||
flags = flags + "=CLASSIFIED"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
err = cmd.Flags().ParseAll(os.Args[1:], parseFunc)
|
||||
if err != nil || !cmd.Flags().Parsed() {
|
||||
return ""
|
||||
}
|
||||
|
||||
args := ""
|
||||
if arguments := cmd.Flags().Args(); len(arguments) > 0 {
|
||||
args = " " + strings.Join(arguments, " ")
|
||||
}
|
||||
|
||||
base := filepath.Base(os.Args[0])
|
||||
return base + args + flags
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 genericclioptions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
)
|
||||
|
||||
// templates are logically optional for specifying a format.
|
||||
// this allows a user to specify a template format value
|
||||
// as --output=go-template=
|
||||
var templateFormats = map[string]bool{
|
||||
"template": true,
|
||||
"go-template": true,
|
||||
"go-template-file": true,
|
||||
"templatefile": true,
|
||||
}
|
||||
|
||||
// GoTemplatePrintFlags provides default flags necessary for template printing.
|
||||
// Given the following flag values, a printer can be requested that knows
|
||||
// how to handle printing based on these values.
|
||||
type GoTemplatePrintFlags struct {
|
||||
// indicates if it is OK to ignore missing keys for rendering
|
||||
// an output template.
|
||||
AllowMissingKeys *bool
|
||||
TemplateArgument *string
|
||||
}
|
||||
|
||||
// AllowedFormats returns slice of string of allowed GoTemplatePrint printing format
|
||||
func (f *GoTemplatePrintFlags) AllowedFormats() []string {
|
||||
formats := make([]string, 0, len(templateFormats))
|
||||
for format := range templateFormats {
|
||||
formats = append(formats, format)
|
||||
}
|
||||
sort.Strings(formats)
|
||||
return formats
|
||||
}
|
||||
|
||||
// ToPrinter receives an templateFormat and returns a printer capable of
|
||||
// handling --template format printing.
|
||||
// Returns false if the specified templateFormat does not match a template format.
|
||||
func (f *GoTemplatePrintFlags) ToPrinter(templateFormat string) (printers.ResourcePrinter, error) {
|
||||
if (f.TemplateArgument == nil || len(*f.TemplateArgument) == 0) && len(templateFormat) == 0 {
|
||||
return nil, NoCompatiblePrinterError{Options: f, OutputFormat: &templateFormat}
|
||||
}
|
||||
|
||||
templateValue := ""
|
||||
|
||||
if f.TemplateArgument == nil || len(*f.TemplateArgument) == 0 {
|
||||
for format := range templateFormats {
|
||||
format = format + "="
|
||||
if strings.HasPrefix(templateFormat, format) {
|
||||
templateValue = templateFormat[len(format):]
|
||||
templateFormat = format[:len(format)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
templateValue = *f.TemplateArgument
|
||||
}
|
||||
|
||||
if _, supportedFormat := templateFormats[templateFormat]; !supportedFormat {
|
||||
return nil, NoCompatiblePrinterError{OutputFormat: &templateFormat, AllowedFormats: f.AllowedFormats()}
|
||||
}
|
||||
|
||||
if len(templateValue) == 0 {
|
||||
return nil, fmt.Errorf("template format specified but no template given")
|
||||
}
|
||||
|
||||
if templateFormat == "templatefile" || templateFormat == "go-template-file" {
|
||||
data, err := os.ReadFile(templateValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading --template %s, %v", templateValue, err)
|
||||
}
|
||||
|
||||
templateValue = string(data)
|
||||
}
|
||||
|
||||
p, err := printers.NewGoTemplatePrinter([]byte(templateValue))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing template %s, %v", templateValue, err)
|
||||
}
|
||||
|
||||
allowMissingKeys := true
|
||||
if f.AllowMissingKeys != nil {
|
||||
allowMissingKeys = *f.AllowMissingKeys
|
||||
}
|
||||
|
||||
p.AllowMissingKeys(allowMissingKeys)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// AddFlags receives a *cobra.Command reference and binds
|
||||
// flags related to template printing to it
|
||||
func (f *GoTemplatePrintFlags) AddFlags(c *cobra.Command) {
|
||||
if f.TemplateArgument != nil {
|
||||
c.Flags().StringVar(f.TemplateArgument, "template", *f.TemplateArgument, "Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].")
|
||||
c.MarkFlagFilename("template")
|
||||
}
|
||||
if f.AllowMissingKeys != nil {
|
||||
c.Flags().BoolVar(f.AllowMissingKeys, "allow-missing-template-keys", *f.AllowMissingKeys, "If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats.")
|
||||
}
|
||||
}
|
||||
|
||||
// NewGoTemplatePrintFlags returns flags associated with
|
||||
// --template printing, with default values set.
|
||||
func NewGoTemplatePrintFlags() *GoTemplatePrintFlags {
|
||||
allowMissingKeysPtr := true
|
||||
templateValuePtr := ""
|
||||
|
||||
return &GoTemplatePrintFlags{
|
||||
TemplateArgument: &templateValuePtr,
|
||||
AllowMissingKeys: &allowMissingKeysPtr,
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
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 genericiooptions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
// IOStreams provides the standard names for iostreams. This is useful for embedding and for unit testing.
|
||||
// Inconsistent and different names make it hard to read and review code
|
||||
type IOStreams struct {
|
||||
// In think, os.Stdin
|
||||
In io.Reader
|
||||
// Out think, os.Stdout
|
||||
Out io.Writer
|
||||
// ErrOut think, os.Stderr
|
||||
ErrOut io.Writer
|
||||
}
|
||||
|
||||
// NewTestIOStreams returns a valid IOStreams and in, out, errout buffers for unit tests
|
||||
func NewTestIOStreams() (IOStreams, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
|
||||
in := &bytes.Buffer{}
|
||||
out := &bytes.Buffer{}
|
||||
errOut := &bytes.Buffer{}
|
||||
|
||||
return IOStreams{
|
||||
In: in,
|
||||
Out: out,
|
||||
ErrOut: errOut,
|
||||
}, in, out, errOut
|
||||
}
|
||||
|
||||
// NewTestIOStreamsDiscard returns a valid IOStreams that just discards
|
||||
func NewTestIOStreamsDiscard() IOStreams {
|
||||
in := &bytes.Buffer{}
|
||||
return IOStreams{
|
||||
In: in,
|
||||
Out: io.Discard,
|
||||
ErrOut: io.Discard,
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// NewDiscardingPrinter is a printer that discards all objects
|
||||
func NewDiscardingPrinter() ResourcePrinterFunc {
|
||||
return ResourcePrinterFunc(func(runtime.Object, io.Writer) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
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 printers is helper for formatting and printing runtime objects into
|
||||
// primitives io.writer.
|
||||
package printers
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// ResourcePrinterFunc is a function that can print objects
|
||||
type ResourcePrinterFunc func(runtime.Object, io.Writer) error
|
||||
|
||||
// PrintObj implements ResourcePrinter
|
||||
func (fn ResourcePrinterFunc) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
return fn(obj, w)
|
||||
}
|
||||
|
||||
// ResourcePrinter is an interface that knows how to print runtime objects.
|
||||
type ResourcePrinter interface {
|
||||
// PrintObj receives a runtime object, formats it and prints it to a writer.
|
||||
PrintObj(runtime.Object, io.Writer) error
|
||||
}
|
||||
|
||||
// PrintOptions struct defines a struct for various print options
|
||||
type PrintOptions struct {
|
||||
NoHeaders bool
|
||||
WithNamespace bool
|
||||
WithKind bool
|
||||
Wide bool
|
||||
ShowLabels bool
|
||||
Kind schema.GroupKind
|
||||
ColumnLabels []string
|
||||
|
||||
SortBy string
|
||||
|
||||
// indicates if it is OK to ignore missing keys for rendering an output template.
|
||||
AllowMissingKeys bool
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// JSONPrinter is an implementation of ResourcePrinter which outputs an object as JSON.
|
||||
type JSONPrinter struct{}
|
||||
|
||||
// PrintObj is an implementation of ResourcePrinter.PrintObj which simply writes the object to the Writer.
|
||||
func (p *JSONPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
// we use reflect.Indirect here in order to obtain the actual value from a pointer.
|
||||
// we need an actual value in order to retrieve the package path for an object.
|
||||
// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
|
||||
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
|
||||
return errors.New(InternalObjectPrinterErr)
|
||||
}
|
||||
|
||||
switch obj := obj.(type) {
|
||||
case *metav1.WatchEvent:
|
||||
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj.Object.Object)).Type().PkgPath()) {
|
||||
return errors.New(InternalObjectPrinterErr)
|
||||
}
|
||||
data, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write([]byte{'\n'})
|
||||
return err
|
||||
case *runtime.Unknown:
|
||||
var buf bytes.Buffer
|
||||
err := json.Indent(&buf, obj.Raw, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteRune('\n')
|
||||
_, err = buf.WriteTo(w)
|
||||
return err
|
||||
}
|
||||
|
||||
if obj.GetObjectKind().GroupVersionKind().Empty() {
|
||||
return fmt.Errorf("missing apiVersion or kind; try GetObjectKind().SetGroupVersionKind() if you know the type")
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(obj, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
_, err = w.Write(data)
|
||||
return err
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/util/jsonpath"
|
||||
)
|
||||
|
||||
// exists returns true if it would be possible to call the index function
|
||||
// with these arguments.
|
||||
//
|
||||
// TODO: how to document this for users?
|
||||
//
|
||||
// index returns the result of indexing its first argument by the following
|
||||
// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
|
||||
// indexed item must be a map, slice, or array.
|
||||
func exists(item interface{}, indices ...interface{}) bool {
|
||||
v := reflect.ValueOf(item)
|
||||
for _, i := range indices {
|
||||
index := reflect.ValueOf(i)
|
||||
var isNil bool
|
||||
if v, isNil = indirect(v); isNil {
|
||||
return false
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.String:
|
||||
var x int64
|
||||
switch index.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
x = index.Int()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
x = int64(index.Uint())
|
||||
default:
|
||||
return false
|
||||
}
|
||||
if x < 0 || x >= int64(v.Len()) {
|
||||
return false
|
||||
}
|
||||
v = v.Index(int(x))
|
||||
case reflect.Map:
|
||||
if !index.IsValid() {
|
||||
index = reflect.Zero(v.Type().Key())
|
||||
}
|
||||
if !index.Type().AssignableTo(v.Type().Key()) {
|
||||
return false
|
||||
}
|
||||
if x := v.MapIndex(index); x.IsValid() {
|
||||
v = x
|
||||
} else {
|
||||
v = reflect.Zero(v.Type().Elem())
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
if _, isNil := indirect(v); isNil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// stolen from text/template
|
||||
// indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
|
||||
// We indirect through pointers and empty interfaces (only) because
|
||||
// non-empty interfaces have methods we might need.
|
||||
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
|
||||
for ; v.Kind() == reflect.Pointer || v.Kind() == reflect.Interface; v = v.Elem() {
|
||||
if v.IsNil() {
|
||||
return v, true
|
||||
}
|
||||
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
|
||||
// JSONPathPrinter is an implementation of ResourcePrinter which formats data with jsonpath expression.
|
||||
type JSONPathPrinter struct {
|
||||
rawTemplate string
|
||||
*jsonpath.JSONPath
|
||||
}
|
||||
|
||||
func NewJSONPathPrinter(tmpl string) (*JSONPathPrinter, error) {
|
||||
j := jsonpath.New("out")
|
||||
if err := j.Parse(tmpl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &JSONPathPrinter{
|
||||
rawTemplate: tmpl,
|
||||
JSONPath: j,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PrintObj formats the obj with the JSONPath Template.
|
||||
func (j *JSONPathPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
// we use reflect.Indirect here in order to obtain the actual value from a pointer.
|
||||
// we need an actual value in order to retrieve the package path for an object.
|
||||
// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
|
||||
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
|
||||
return errors.New(InternalObjectPrinterErr)
|
||||
}
|
||||
|
||||
var queryObj interface{} = obj
|
||||
if unstructured, ok := obj.(runtime.Unstructured); ok {
|
||||
queryObj = unstructured.UnstructuredContent()
|
||||
} else {
|
||||
data, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryObj = map[string]interface{}{}
|
||||
if err := json.Unmarshal(data, &queryObj); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := j.JSONPath.Execute(w, queryObj); err != nil {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
fmt.Fprintf(buf, "Error executing template: %v. Printing more information for debugging the template:\n", err)
|
||||
fmt.Fprintf(buf, "\ttemplate was:\n\t\t%v\n", j.rawTemplate)
|
||||
fmt.Fprintf(buf, "\tobject given to jsonpath engine was:\n\t\t%#v\n\n", queryObj)
|
||||
return fmt.Errorf("error executing jsonpath %q: %v\n", j.rawTemplate, buf.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2025 The Kubernetes Authors.
|
||||
|
||||
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 kyaml provides a printer for Kubernetes objects that formats them
|
||||
// as KYAML, a strict subset of YAML that is designed to be explicit and
|
||||
// unambiguous. KYAML is YAML.
|
||||
package printers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/yaml/kyaml"
|
||||
)
|
||||
|
||||
// KYAMLPrinter is an implementation of ResourcePrinter which formats data into
|
||||
// a specific dialect of YAML, known as KYAML. KYAML is halfway between YAML
|
||||
// and JSON, but is a strict subset of YAML, and so it should should be
|
||||
// readable by any YAML parser. It is designed to be explicit and unambiguous,
|
||||
// and eschews significant whitespace.
|
||||
type KYAMLPrinter struct {
|
||||
encoder kyaml.Encoder
|
||||
}
|
||||
|
||||
// PrintObj prints the data as KYAML to the specified writer.
|
||||
func (p *KYAMLPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
// We use reflect.Indirect here in order to obtain the actual value from a pointer.
|
||||
// We need an actual value in order to retrieve the package path for an object.
|
||||
// Using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
|
||||
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
|
||||
return errors.New(InternalObjectPrinterErr)
|
||||
}
|
||||
|
||||
switch obj := obj.(type) {
|
||||
case *metav1.WatchEvent:
|
||||
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj.Object.Object)).Type().PkgPath()) {
|
||||
return errors.New(InternalObjectPrinterErr)
|
||||
}
|
||||
case *runtime.Unknown:
|
||||
return p.encoder.FromYAML(bytes.NewReader(obj.Raw), w)
|
||||
}
|
||||
|
||||
if obj.GetObjectKind().GroupVersionKind().Empty() {
|
||||
return fmt.Errorf("missing apiVersion or kind; try GetObjectKind().SetGroupVersionKind() if you know the type")
|
||||
}
|
||||
|
||||
return p.encoder.FromObject(obj, w)
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// OmitManagedFieldsPrinter wraps an existing printer and omits the managed fields from the object
|
||||
// before printing it.
|
||||
type OmitManagedFieldsPrinter struct {
|
||||
Delegate ResourcePrinter
|
||||
}
|
||||
|
||||
var _ ResourcePrinter = (*OmitManagedFieldsPrinter)(nil)
|
||||
|
||||
func omitManagedFields(o runtime.Object) runtime.Object {
|
||||
a, err := meta.Accessor(o)
|
||||
if err != nil {
|
||||
// The object is not a `metav1.Object`, ignore it.
|
||||
return o
|
||||
}
|
||||
a.SetManagedFields(nil)
|
||||
return o
|
||||
}
|
||||
|
||||
// PrintObj copies the object and omits the managed fields from the copied object before printing it.
|
||||
func (p *OmitManagedFieldsPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
if obj == nil {
|
||||
return p.Delegate.PrintObj(obj, w)
|
||||
}
|
||||
if meta.IsListType(obj) {
|
||||
obj = obj.DeepCopyObject()
|
||||
_ = meta.EachListItem(obj, func(item runtime.Object) error {
|
||||
omitManagedFields(item)
|
||||
return nil
|
||||
})
|
||||
} else if _, err := meta.Accessor(obj); err == nil {
|
||||
obj = omitManagedFields(obj.DeepCopyObject())
|
||||
}
|
||||
return p.Delegate.PrintObj(obj, w)
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// NamePrinter is an implementation of ResourcePrinter which outputs "resource/name" pair of an object.
|
||||
type NamePrinter struct {
|
||||
// ShortOutput indicates whether an operation should be
|
||||
// printed along side the "resource/name" pair for an object.
|
||||
ShortOutput bool
|
||||
// Operation describes the name of the action that
|
||||
// took place on an object, to be included in the
|
||||
// finalized "successful" message.
|
||||
Operation string
|
||||
}
|
||||
|
||||
// PrintObj is an implementation of ResourcePrinter.PrintObj which decodes the object
|
||||
// and print "resource/name" pair. If the object is a List, print all items in it.
|
||||
func (p *NamePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
switch castObj := obj.(type) {
|
||||
case *metav1.WatchEvent:
|
||||
obj = castObj.Object.Object
|
||||
}
|
||||
|
||||
// we use reflect.Indirect here in order to obtain the actual value from a pointer.
|
||||
// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
|
||||
// we need an actual value in order to retrieve the package path for an object.
|
||||
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
|
||||
return errors.New(InternalObjectPrinterErr)
|
||||
}
|
||||
|
||||
if meta.IsListType(obj) {
|
||||
// we allow unstructured lists for now because they always contain the GVK information. We should chase down
|
||||
// callers and stop them from passing unflattened lists
|
||||
// TODO chase the caller that is setting this and remove it.
|
||||
if _, ok := obj.(*unstructured.UnstructuredList); !ok {
|
||||
return fmt.Errorf("list types are not supported by name printing: %T", obj)
|
||||
}
|
||||
|
||||
items, err := meta.ExtractList(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, obj := range items {
|
||||
if err := p.PrintObj(obj, w); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if obj.GetObjectKind().GroupVersionKind().Empty() {
|
||||
return fmt.Errorf("missing apiVersion or kind; try GetObjectKind().SetGroupVersionKind() if you know the type")
|
||||
}
|
||||
|
||||
name := "<unknown>"
|
||||
if acc, err := meta.Accessor(obj); err == nil {
|
||||
if n := acc.GetName(); len(n) > 0 {
|
||||
name = n
|
||||
}
|
||||
}
|
||||
|
||||
return printObj(w, name, p.Operation, p.ShortOutput, GetObjectGroupKind(obj))
|
||||
}
|
||||
|
||||
func GetObjectGroupKind(obj runtime.Object) schema.GroupKind {
|
||||
if obj == nil {
|
||||
return schema.GroupKind{Kind: "<unknown>"}
|
||||
}
|
||||
groupVersionKind := obj.GetObjectKind().GroupVersionKind()
|
||||
if len(groupVersionKind.Kind) > 0 {
|
||||
return groupVersionKind.GroupKind()
|
||||
}
|
||||
|
||||
if uns, ok := obj.(*unstructured.Unstructured); ok {
|
||||
if len(uns.GroupVersionKind().Kind) > 0 {
|
||||
return uns.GroupVersionKind().GroupKind()
|
||||
}
|
||||
}
|
||||
|
||||
return schema.GroupKind{Kind: "<unknown>"}
|
||||
}
|
||||
|
||||
func printObj(w io.Writer, name string, operation string, shortOutput bool, groupKind schema.GroupKind) error {
|
||||
if len(groupKind.Kind) == 0 {
|
||||
return fmt.Errorf("missing kind for resource with name %v", name)
|
||||
}
|
||||
|
||||
if len(operation) > 0 {
|
||||
operation = " " + operation
|
||||
}
|
||||
|
||||
if shortOutput {
|
||||
operation = ""
|
||||
}
|
||||
|
||||
if len(groupKind.Group) == 0 {
|
||||
fmt.Fprintf(w, "%s/%s%s\n", strings.ToLower(groupKind.Kind), name, operation)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s.%s/%s%s\n", strings.ToLower(groupKind.Kind), groupKind.Group, name, operation)
|
||||
return nil
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
InternalObjectPrinterErr = "a versioned object must be passed to a printer"
|
||||
|
||||
// disallowedPackagePrefixes contains regular expression templates
|
||||
// for object package paths that are not allowed by printers.
|
||||
disallowedPackagePrefixes = []string{
|
||||
"k8s.io/kubernetes/pkg/apis/",
|
||||
}
|
||||
)
|
||||
|
||||
var InternalObjectPreventer = &illegalPackageSourceChecker{disallowedPackagePrefixes}
|
||||
|
||||
func IsInternalObjectError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return err.Error() == InternalObjectPrinterErr
|
||||
}
|
||||
|
||||
// illegalPackageSourceChecker compares a given
|
||||
// object's package path, and determines if the
|
||||
// object originates from a disallowed source.
|
||||
type illegalPackageSourceChecker struct {
|
||||
// disallowedPrefixes is a slice of disallowed package path
|
||||
// prefixes for a given runtime.Object that we are printing.
|
||||
disallowedPrefixes []string
|
||||
}
|
||||
|
||||
func (c *illegalPackageSourceChecker) IsForbidden(pkgPath string) bool {
|
||||
for _, forbiddenPrefix := range c.disallowedPrefixes {
|
||||
if strings.HasPrefix(pkgPath, forbiddenPrefix) || strings.Contains(pkgPath, "/vendor/"+forbiddenPrefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
+589
@@ -0,0 +1,589 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/liggitt/tabwriter"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/duration"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
var _ ResourcePrinter = &HumanReadablePrinter{}
|
||||
|
||||
type printHandler struct {
|
||||
columnDefinitions []metav1.TableColumnDefinition
|
||||
printFunc reflect.Value
|
||||
}
|
||||
|
||||
var (
|
||||
statusHandlerEntry = &printHandler{
|
||||
columnDefinitions: statusColumnDefinitions,
|
||||
printFunc: reflect.ValueOf(printStatus),
|
||||
}
|
||||
|
||||
statusColumnDefinitions = []metav1.TableColumnDefinition{
|
||||
{Name: "Status", Type: "string"},
|
||||
{Name: "Reason", Type: "string"},
|
||||
{Name: "Message", Type: "string"},
|
||||
}
|
||||
|
||||
defaultHandlerEntry = &printHandler{
|
||||
columnDefinitions: objectMetaColumnDefinitions,
|
||||
printFunc: reflect.ValueOf(printObjectMeta),
|
||||
}
|
||||
|
||||
objectMetaColumnDefinitions = []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
|
||||
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
|
||||
}
|
||||
|
||||
withEventTypePrefixColumns = []string{"EVENT"}
|
||||
withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too.
|
||||
)
|
||||
|
||||
// HumanReadablePrinter is an implementation of ResourcePrinter which attempts to provide
|
||||
// more elegant output. It is not threadsafe, but you may call PrintObj repeatedly; headers
|
||||
// will only be printed if the object type changes. This makes it useful for printing items
|
||||
// received from watches.
|
||||
type HumanReadablePrinter struct {
|
||||
options PrintOptions
|
||||
lastType interface{}
|
||||
lastColumns []metav1.TableColumnDefinition
|
||||
printedHeaders bool
|
||||
}
|
||||
|
||||
// NewTablePrinter creates a printer suitable for calling PrintObj().
|
||||
func NewTablePrinter(options PrintOptions) ResourcePrinter {
|
||||
printer := &HumanReadablePrinter{
|
||||
options: options,
|
||||
}
|
||||
return printer
|
||||
}
|
||||
|
||||
func printHeader(columnNames []string, w io.Writer) error {
|
||||
if _, err := fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintObj prints the obj in a human-friendly format according to the type of the obj.
|
||||
func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) error {
|
||||
|
||||
if _, found := output.(*tabwriter.Writer); !found {
|
||||
w := GetNewTabWriter(output)
|
||||
output = w
|
||||
defer w.Flush()
|
||||
}
|
||||
|
||||
var eventType string
|
||||
if event, isEvent := obj.(*metav1.WatchEvent); isEvent {
|
||||
eventType = event.Type
|
||||
obj = event.Object.Object
|
||||
}
|
||||
|
||||
// Parameter "obj" is a table from server; print it.
|
||||
// display tables following the rules of options
|
||||
if table, ok := obj.(*metav1.Table); ok {
|
||||
// Do not print headers if this table has no column definitions, or they are the same as the last ones we printed
|
||||
localOptions := h.options
|
||||
if h.printedHeaders && (len(table.ColumnDefinitions) == 0 || reflect.DeepEqual(table.ColumnDefinitions, h.lastColumns)) {
|
||||
localOptions.NoHeaders = true
|
||||
}
|
||||
|
||||
if len(table.ColumnDefinitions) == 0 {
|
||||
// If this table has no column definitions, use the columns from the last table we printed for decoration and layout.
|
||||
// This is done when receiving tables in watch events to save bandwidth.
|
||||
table.ColumnDefinitions = h.lastColumns
|
||||
} else if !reflect.DeepEqual(table.ColumnDefinitions, h.lastColumns) {
|
||||
// If this table has column definitions, remember them for future use.
|
||||
h.lastColumns = table.ColumnDefinitions
|
||||
h.printedHeaders = false
|
||||
}
|
||||
|
||||
if len(table.Rows) > 0 {
|
||||
h.printedHeaders = true
|
||||
}
|
||||
|
||||
if err := decorateTable(table, localOptions); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(eventType) > 0 {
|
||||
if err := addColumns(beginning, table,
|
||||
[]metav1.TableColumnDefinition{{Name: "Event", Type: "string"}},
|
||||
[]cellValueFunc{func(metav1.TableRow) (interface{}, error) { return formatEventType(eventType), nil }},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return printTable(table, output, localOptions)
|
||||
}
|
||||
|
||||
// Could not find print handler for "obj"; use the default or status print handler.
|
||||
// Print with the default or status handler, and use the columns from the last time
|
||||
var handler *printHandler
|
||||
if _, isStatus := obj.(*metav1.Status); isStatus {
|
||||
handler = statusHandlerEntry
|
||||
} else {
|
||||
handler = defaultHandlerEntry
|
||||
}
|
||||
|
||||
includeHeaders := h.lastType != handler && !h.options.NoHeaders
|
||||
|
||||
if h.lastType != nil && h.lastType != handler && !h.options.NoHeaders {
|
||||
fmt.Fprintln(output)
|
||||
}
|
||||
|
||||
if err := printRowsForHandlerEntry(output, handler, eventType, obj, h.options, includeHeaders); err != nil {
|
||||
return err
|
||||
}
|
||||
h.lastType = handler
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// printTable prints a table to the provided output respecting the filtering rules for options
|
||||
// for wide columns and filtered rows. It filters out rows that are Completed. You should call
|
||||
// decorateTable if you receive a table from a remote server before calling printTable.
|
||||
func printTable(table *metav1.Table, output io.Writer, options PrintOptions) error {
|
||||
if !options.NoHeaders {
|
||||
// avoid printing headers if we have no rows to display
|
||||
if len(table.Rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
first := true
|
||||
for _, column := range table.ColumnDefinitions {
|
||||
if !options.Wide && column.Priority != 0 {
|
||||
continue
|
||||
}
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
fmt.Fprint(output, "\t")
|
||||
}
|
||||
fmt.Fprint(output, strings.ToUpper(column.Name))
|
||||
}
|
||||
fmt.Fprintln(output)
|
||||
}
|
||||
for _, row := range table.Rows {
|
||||
first := true
|
||||
for i, cell := range row.Cells {
|
||||
if i >= len(table.ColumnDefinitions) {
|
||||
// https://issue.k8s.io/66379
|
||||
// don't panic in case of bad output from the server, with more cells than column definitions
|
||||
break
|
||||
}
|
||||
column := table.ColumnDefinitions[i]
|
||||
if !options.Wide && column.Priority != 0 {
|
||||
continue
|
||||
}
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
fmt.Fprint(output, "\t")
|
||||
}
|
||||
if cell != nil {
|
||||
switch val := cell.(type) {
|
||||
case string:
|
||||
print := val
|
||||
truncated := false
|
||||
// Truncate at the first newline, carriage return or formfeed
|
||||
// (treated as a newline by tabwriter).
|
||||
breakchar := strings.IndexAny(print, "\f\n\r")
|
||||
if breakchar >= 0 {
|
||||
truncated = true
|
||||
print = print[:breakchar]
|
||||
}
|
||||
WriteEscaped(output, print)
|
||||
if truncated {
|
||||
fmt.Fprint(output, "...")
|
||||
}
|
||||
default:
|
||||
WriteEscaped(output, fmt.Sprint(val))
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type cellValueFunc func(metav1.TableRow) (interface{}, error)
|
||||
|
||||
type columnAddPosition int
|
||||
|
||||
const (
|
||||
beginning columnAddPosition = 1
|
||||
end columnAddPosition = 2
|
||||
)
|
||||
|
||||
func addColumns(pos columnAddPosition, table *metav1.Table, columns []metav1.TableColumnDefinition, valueFuncs []cellValueFunc) error {
|
||||
if len(columns) != len(valueFuncs) {
|
||||
return fmt.Errorf("cannot prepend columns, unmatched value functions")
|
||||
}
|
||||
if len(columns) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compute the new rows
|
||||
newRows := make([][]interface{}, len(table.Rows))
|
||||
for i := range table.Rows {
|
||||
newCells := make([]interface{}, 0, len(columns)+len(table.Rows[i].Cells))
|
||||
|
||||
if pos == end {
|
||||
// If we're appending, start with the existing cells,
|
||||
// then add nil cells to match the number of columns
|
||||
newCells = append(newCells, table.Rows[i].Cells...)
|
||||
for len(newCells) < len(table.ColumnDefinitions) {
|
||||
newCells = append(newCells, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Compute cells for new columns
|
||||
for _, f := range valueFuncs {
|
||||
newCell, err := f(table.Rows[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newCells = append(newCells, newCell)
|
||||
}
|
||||
|
||||
if pos == beginning {
|
||||
// If we're prepending, add existing cells
|
||||
newCells = append(newCells, table.Rows[i].Cells...)
|
||||
}
|
||||
|
||||
// Remember the new cells for this row
|
||||
newRows[i] = newCells
|
||||
}
|
||||
|
||||
// All cells successfully computed, now replace columns and rows
|
||||
newColumns := make([]metav1.TableColumnDefinition, 0, len(columns)+len(table.ColumnDefinitions))
|
||||
switch pos {
|
||||
case beginning:
|
||||
newColumns = append(newColumns, columns...)
|
||||
newColumns = append(newColumns, table.ColumnDefinitions...)
|
||||
case end:
|
||||
newColumns = append(newColumns, table.ColumnDefinitions...)
|
||||
newColumns = append(newColumns, columns...)
|
||||
default:
|
||||
return fmt.Errorf("invalid column add position: %v", pos)
|
||||
}
|
||||
table.ColumnDefinitions = newColumns
|
||||
for i := range table.Rows {
|
||||
table.Rows[i].Cells = newRows[i]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// decorateTable takes a table and attempts to add label columns and the
|
||||
// namespace column. It will fill empty columns with nil (if the object
|
||||
// does not expose metadata). It returns an error if the table cannot
|
||||
// be decorated.
|
||||
func decorateTable(table *metav1.Table, options PrintOptions) error {
|
||||
width := len(table.ColumnDefinitions) + len(options.ColumnLabels)
|
||||
if options.WithNamespace {
|
||||
width++
|
||||
}
|
||||
if options.ShowLabels {
|
||||
width++
|
||||
}
|
||||
|
||||
columns := table.ColumnDefinitions
|
||||
|
||||
nameColumn := -1
|
||||
if options.WithKind && !options.Kind.Empty() {
|
||||
for i := range columns {
|
||||
if columns[i].Format == "name" && columns[i].Type == "string" {
|
||||
nameColumn = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if width != len(table.ColumnDefinitions) {
|
||||
columns = make([]metav1.TableColumnDefinition, 0, width)
|
||||
if options.WithNamespace {
|
||||
columns = append(columns, metav1.TableColumnDefinition{
|
||||
Name: "Namespace",
|
||||
Type: "string",
|
||||
})
|
||||
}
|
||||
columns = append(columns, table.ColumnDefinitions...)
|
||||
for _, label := range formatLabelHeaders(options.ColumnLabels) {
|
||||
columns = append(columns, metav1.TableColumnDefinition{
|
||||
Name: label,
|
||||
Type: "string",
|
||||
})
|
||||
}
|
||||
if options.ShowLabels {
|
||||
columns = append(columns, metav1.TableColumnDefinition{
|
||||
Name: "Labels",
|
||||
Type: "string",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
rows := table.Rows
|
||||
|
||||
includeLabels := len(options.ColumnLabels) > 0 || options.ShowLabels
|
||||
if includeLabels || options.WithNamespace || nameColumn != -1 {
|
||||
for i := range rows {
|
||||
row := rows[i]
|
||||
|
||||
if nameColumn != -1 {
|
||||
row.Cells[nameColumn] = fmt.Sprintf("%s/%s", strings.ToLower(options.Kind.String()), row.Cells[nameColumn])
|
||||
}
|
||||
|
||||
var m metav1.Object
|
||||
if obj := row.Object.Object; obj != nil {
|
||||
if acc, err := meta.Accessor(obj); err == nil {
|
||||
m = acc
|
||||
}
|
||||
}
|
||||
// if we can't get an accessor, fill out the appropriate columns with empty spaces
|
||||
if m == nil {
|
||||
if options.WithNamespace {
|
||||
r := make([]interface{}, 1, width)
|
||||
row.Cells = append(r, row.Cells...)
|
||||
}
|
||||
for j := 0; j < width-len(row.Cells); j++ {
|
||||
row.Cells = append(row.Cells, nil)
|
||||
}
|
||||
rows[i] = row
|
||||
continue
|
||||
}
|
||||
|
||||
if options.WithNamespace {
|
||||
r := make([]interface{}, 1, width)
|
||||
r[0] = m.GetNamespace()
|
||||
row.Cells = append(r, row.Cells...)
|
||||
}
|
||||
if includeLabels {
|
||||
row.Cells = appendLabelCells(row.Cells, m.GetLabels(), options)
|
||||
}
|
||||
rows[i] = row
|
||||
}
|
||||
}
|
||||
|
||||
table.ColumnDefinitions = columns
|
||||
table.Rows = rows
|
||||
return nil
|
||||
}
|
||||
|
||||
// printRowsForHandlerEntry prints the incremental table output (headers if the current type is
|
||||
// different from lastType) including all the rows in the object. It returns the current type
|
||||
// or an error, if any.
|
||||
func printRowsForHandlerEntry(output io.Writer, handler *printHandler, eventType string, obj runtime.Object, options PrintOptions, includeHeaders bool) error {
|
||||
var results []reflect.Value
|
||||
|
||||
args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(options)}
|
||||
results = handler.printFunc.Call(args)
|
||||
if !results[1].IsNil() {
|
||||
return results[1].Interface().(error)
|
||||
}
|
||||
|
||||
if includeHeaders {
|
||||
var headers []string
|
||||
for _, column := range handler.columnDefinitions {
|
||||
if column.Priority != 0 && !options.Wide {
|
||||
continue
|
||||
}
|
||||
headers = append(headers, strings.ToUpper(column.Name))
|
||||
}
|
||||
headers = append(headers, formatLabelHeaders(options.ColumnLabels)...)
|
||||
// LABELS is always the last column.
|
||||
headers = append(headers, formatShowLabelsHeader(options.ShowLabels)...)
|
||||
// prepend namespace header
|
||||
if options.WithNamespace {
|
||||
headers = append(withNamespacePrefixColumns, headers...)
|
||||
}
|
||||
// prepend event type header
|
||||
if len(eventType) > 0 {
|
||||
headers = append(withEventTypePrefixColumns, headers...)
|
||||
}
|
||||
printHeader(headers, output)
|
||||
}
|
||||
|
||||
if results[1].IsNil() {
|
||||
rows := results[0].Interface().([]metav1.TableRow)
|
||||
printRows(output, eventType, rows, options)
|
||||
return nil
|
||||
}
|
||||
return results[1].Interface().(error)
|
||||
}
|
||||
|
||||
var formattedEventType = map[string]string{
|
||||
string(watch.Added): "ADDED ",
|
||||
string(watch.Modified): "MODIFIED",
|
||||
string(watch.Deleted): "DELETED ",
|
||||
string(watch.Error): "ERROR ",
|
||||
}
|
||||
|
||||
func formatEventType(eventType string) string {
|
||||
if formatted, ok := formattedEventType[eventType]; ok {
|
||||
return formatted
|
||||
}
|
||||
return eventType
|
||||
}
|
||||
|
||||
// printRows writes the provided rows to output.
|
||||
func printRows(output io.Writer, eventType string, rows []metav1.TableRow, options PrintOptions) {
|
||||
for _, row := range rows {
|
||||
if len(eventType) > 0 {
|
||||
fmt.Fprint(output, formatEventType(eventType))
|
||||
fmt.Fprint(output, "\t")
|
||||
}
|
||||
if options.WithNamespace {
|
||||
if obj := row.Object.Object; obj != nil {
|
||||
if m, err := meta.Accessor(obj); err == nil {
|
||||
fmt.Fprint(output, m.GetNamespace())
|
||||
}
|
||||
}
|
||||
fmt.Fprint(output, "\t")
|
||||
}
|
||||
|
||||
for i, cell := range row.Cells {
|
||||
if i != 0 {
|
||||
fmt.Fprint(output, "\t")
|
||||
} else {
|
||||
// TODO: remove this once we drop the legacy printers
|
||||
if options.WithKind && !options.Kind.Empty() {
|
||||
fmt.Fprintf(output, "%s/%s", strings.ToLower(options.Kind.String()), cell)
|
||||
continue
|
||||
}
|
||||
}
|
||||
fmt.Fprint(output, cell)
|
||||
}
|
||||
|
||||
hasLabels := len(options.ColumnLabels) > 0
|
||||
if obj := row.Object.Object; obj != nil && (hasLabels || options.ShowLabels) {
|
||||
if m, err := meta.Accessor(obj); err == nil {
|
||||
for _, value := range labelValues(m.GetLabels(), options) {
|
||||
output.Write([]byte("\t"))
|
||||
output.Write([]byte(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func formatLabelHeaders(columnLabels []string) []string {
|
||||
formHead := make([]string, len(columnLabels))
|
||||
for i, l := range columnLabels {
|
||||
p := strings.Split(l, "/")
|
||||
formHead[i] = strings.ToUpper(p[len(p)-1])
|
||||
}
|
||||
return formHead
|
||||
}
|
||||
|
||||
// headers for --show-labels=true
|
||||
func formatShowLabelsHeader(showLabels bool) []string {
|
||||
if showLabels {
|
||||
return []string{"LABELS"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// labelValues returns a slice of value columns matching the requested print options.
|
||||
func labelValues(itemLabels map[string]string, opts PrintOptions) []string {
|
||||
var values []string
|
||||
for _, key := range opts.ColumnLabels {
|
||||
values = append(values, itemLabels[key])
|
||||
}
|
||||
if opts.ShowLabels {
|
||||
values = append(values, labels.FormatLabels(itemLabels))
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// appendLabelCells returns a slice of value columns matching the requested print options.
|
||||
// Intended for use with tables.
|
||||
func appendLabelCells(values []interface{}, itemLabels map[string]string, opts PrintOptions) []interface{} {
|
||||
for _, key := range opts.ColumnLabels {
|
||||
values = append(values, itemLabels[key])
|
||||
}
|
||||
if opts.ShowLabels {
|
||||
values = append(values, labels.FormatLabels(itemLabels))
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func printStatus(obj runtime.Object, options PrintOptions) ([]metav1.TableRow, error) {
|
||||
status, ok := obj.(*metav1.Status)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected *v1.Status, got %T", obj)
|
||||
}
|
||||
return []metav1.TableRow{{
|
||||
Object: runtime.RawExtension{Object: obj},
|
||||
Cells: []interface{}{status.Status, status.Reason, status.Message},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func printObjectMeta(obj runtime.Object, options PrintOptions) ([]metav1.TableRow, error) {
|
||||
if meta.IsListType(obj) {
|
||||
rows := make([]metav1.TableRow, 0, 16)
|
||||
err := meta.EachListItem(obj, func(obj runtime.Object) error {
|
||||
nestedRows, err := printObjectMeta(obj, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows = append(rows, nestedRows...)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
rows := make([]metav1.TableRow, 0, 1)
|
||||
m, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row := metav1.TableRow{
|
||||
Object: runtime.RawExtension{Object: obj},
|
||||
}
|
||||
row.Cells = append(row.Cells, m.GetName(), translateTimestampSince(m.GetCreationTimestamp()))
|
||||
rows = append(rows, row)
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// translateTimestampSince returns the elapsed time since timestamp in
|
||||
// human-readable approximation.
|
||||
func translateTimestampSince(timestamp metav1.Time) string {
|
||||
if timestamp.IsZero() {
|
||||
return "<unknown>"
|
||||
}
|
||||
|
||||
return duration.HumanDuration(time.Since(timestamp.Time))
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/liggitt/tabwriter"
|
||||
)
|
||||
|
||||
const (
|
||||
tabwriterMinWidth = 6
|
||||
tabwriterWidth = 4
|
||||
tabwriterPadding = 3
|
||||
tabwriterPadChar = ' '
|
||||
tabwriterFlags = tabwriter.RememberWidths
|
||||
)
|
||||
|
||||
// GetNewTabWriter returns a tabwriter that translates tabbed columns in input into properly aligned text.
|
||||
func GetNewTabWriter(output io.Writer) *tabwriter.Writer {
|
||||
return tabwriter.NewWriter(output, tabwriterMinWidth, tabwriterWidth, tabwriterPadding, tabwriterPadChar, tabwriterFlags)
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"text/template"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
)
|
||||
|
||||
// GoTemplatePrinter is an implementation of ResourcePrinter which formats data with a Go Template.
|
||||
type GoTemplatePrinter struct {
|
||||
rawTemplate string
|
||||
template *template.Template
|
||||
}
|
||||
|
||||
func NewGoTemplatePrinter(tmpl []byte) (*GoTemplatePrinter, error) {
|
||||
t, err := template.New("output").
|
||||
Funcs(template.FuncMap{
|
||||
"exists": exists,
|
||||
"base64decode": base64decode,
|
||||
}).
|
||||
Parse(string(tmpl))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GoTemplatePrinter{
|
||||
rawTemplate: string(tmpl),
|
||||
template: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AllowMissingKeys tells the template engine if missing keys are allowed.
|
||||
func (p *GoTemplatePrinter) AllowMissingKeys(allow bool) {
|
||||
if allow {
|
||||
p.template.Option("missingkey=default")
|
||||
} else {
|
||||
p.template.Option("missingkey=error")
|
||||
}
|
||||
}
|
||||
|
||||
// PrintObj formats the obj with the Go Template.
|
||||
func (p *GoTemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
|
||||
return errors.New(InternalObjectPrinterErr)
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
data, err = json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out := map[string]interface{}{}
|
||||
if err := json.Unmarshal(data, &out); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.safeExecute(w, out); err != nil {
|
||||
// It is way easier to debug this stuff when it shows up in
|
||||
// stdout instead of just stdin. So in addition to returning
|
||||
// a nice error, also print useful stuff with the writer.
|
||||
fmt.Fprintf(w, "Error executing template: %v. Printing more information for debugging the template:\n", err)
|
||||
fmt.Fprintf(w, "\ttemplate was:\n\t\t%v\n", p.rawTemplate)
|
||||
fmt.Fprintf(w, "\traw data was:\n\t\t%v\n", string(data))
|
||||
fmt.Fprintf(w, "\tobject given to template engine was:\n\t\t%+v\n\n", out)
|
||||
return fmt.Errorf("error executing template %q: %v", p.rawTemplate, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// safeExecute tries to execute the template, but catches panics and returns an error
|
||||
// should the template engine panic.
|
||||
func (p *GoTemplatePrinter) safeExecute(w io.Writer, obj interface{}) error {
|
||||
var panicErr error
|
||||
// Sorry for the double anonymous function. There's probably a clever way
|
||||
// to do this that has the defer'd func setting the value to be returned, but
|
||||
// that would be even less obvious.
|
||||
retErr := func() error {
|
||||
defer func() {
|
||||
if x := recover(); x != nil {
|
||||
panicErr = fmt.Errorf("caught panic: %+v", x)
|
||||
}
|
||||
}()
|
||||
return p.template.Execute(w, obj)
|
||||
}()
|
||||
if panicErr != nil {
|
||||
return panicErr
|
||||
}
|
||||
return retErr
|
||||
}
|
||||
|
||||
func base64decode(v string) (string, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(v)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("base64 decode failed: %v", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/term"
|
||||
)
|
||||
|
||||
// terminalEscaper replaces ANSI escape sequences and other terminal special
|
||||
// characters to avoid terminal escape character attacks (issue #101695).
|
||||
var terminalEscaper = strings.NewReplacer("\x1b", "^[", "\r", "\\r")
|
||||
|
||||
// WriteEscaped replaces unsafe terminal characters with replacement strings
|
||||
// and writes them to the given writer.
|
||||
func WriteEscaped(writer io.Writer, output string) error {
|
||||
_, err := terminalEscaper.WriteString(writer, output)
|
||||
return err
|
||||
}
|
||||
|
||||
// EscapeTerminal escapes terminal special characters in a human readable (but
|
||||
// non-reversible) format.
|
||||
func EscapeTerminal(in string) string {
|
||||
return terminalEscaper.Replace(in)
|
||||
}
|
||||
|
||||
// IsTerminal returns whether the passed object is a terminal or not
|
||||
func IsTerminal(i interface{}) bool {
|
||||
_, terminal := term.GetFdInfo(i)
|
||||
return terminal
|
||||
}
|
||||
|
||||
// AllowsColorOutput returns true if the specified writer is a terminal and
|
||||
// the process environment indicates color output is supported and desired.
|
||||
func AllowsColorOutput(w io.Writer) bool {
|
||||
if !IsTerminal(w) {
|
||||
return false
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Computer_terminal#Dumb_terminals
|
||||
if os.Getenv("TERM") == "dumb" {
|
||||
return false
|
||||
}
|
||||
|
||||
// https://no-color.org/
|
||||
if _, nocolor := os.LookupEnv("NO_COLOR"); nocolor {
|
||||
return false
|
||||
}
|
||||
|
||||
// On Windows WT_SESSION is set by the modern terminal component.
|
||||
// Older terminals have poor support for UTF-8, VT escape codes, etc.
|
||||
if runtime.GOOS == "windows" && os.Getenv("WT_SESSION") == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// TypeSetterPrinter is an implementation of ResourcePrinter wraps another printer with types set on the objects
|
||||
type TypeSetterPrinter struct {
|
||||
Delegate ResourcePrinter
|
||||
|
||||
Typer runtime.ObjectTyper
|
||||
}
|
||||
|
||||
// NewTypeSetter constructs a wrapping printer with required params
|
||||
func NewTypeSetter(typer runtime.ObjectTyper) *TypeSetterPrinter {
|
||||
return &TypeSetterPrinter{Typer: typer}
|
||||
}
|
||||
|
||||
// PrintObj is an implementation of ResourcePrinter.PrintObj which sets type information on the obj for the duration
|
||||
// of printing. It is NOT threadsafe.
|
||||
func (p *TypeSetterPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
if obj == nil {
|
||||
return p.Delegate.PrintObj(obj, w)
|
||||
}
|
||||
if !obj.GetObjectKind().GroupVersionKind().Empty() {
|
||||
return p.Delegate.PrintObj(obj, w)
|
||||
}
|
||||
|
||||
// we were empty coming in, make sure we're empty going out. This makes the call thread-unsafe
|
||||
defer func() {
|
||||
obj.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{})
|
||||
}()
|
||||
|
||||
gvks, _, err := p.Typer.ObjectKinds(obj)
|
||||
if err != nil {
|
||||
// printers wrapped by us expect to find the type information present
|
||||
return fmt.Errorf("missing apiVersion or kind and cannot assign it; %v", err)
|
||||
}
|
||||
|
||||
for _, gvk := range gvks {
|
||||
if len(gvk.Kind) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(gvk.Version) == 0 || gvk.Version == runtime.APIVersionInternal {
|
||||
continue
|
||||
}
|
||||
obj.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
break
|
||||
}
|
||||
|
||||
return p.Delegate.PrintObj(obj, w)
|
||||
}
|
||||
|
||||
// ToPrinter returns a printer (not threadsafe!) that has been wrapped
|
||||
func (p *TypeSetterPrinter) ToPrinter(delegate ResourcePrinter) ResourcePrinter {
|
||||
if p == nil {
|
||||
return delegate
|
||||
}
|
||||
|
||||
p.Delegate = delegate
|
||||
return p
|
||||
}
|
||||
|
||||
// WrapToPrinter wraps the common ToPrinter method
|
||||
func (p *TypeSetterPrinter) WrapToPrinter(delegate ResourcePrinter, err error) (ResourcePrinter, error) {
|
||||
if err != nil {
|
||||
return delegate, err
|
||||
}
|
||||
if p == nil {
|
||||
return delegate, nil
|
||||
}
|
||||
|
||||
p.Delegate = delegate
|
||||
return p, nil
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
yellowColor = "\u001b[33;1m"
|
||||
resetColor = "\u001b[0m"
|
||||
)
|
||||
|
||||
type WarningPrinter struct {
|
||||
// out is the writer to output warnings to
|
||||
out io.Writer
|
||||
// opts contains options controlling warning output
|
||||
opts WarningPrinterOptions
|
||||
}
|
||||
|
||||
// WarningPrinterOptions controls the behavior of a WarningPrinter constructed using NewWarningPrinter()
|
||||
type WarningPrinterOptions struct {
|
||||
// Color indicates that warning output can include ANSI color codes
|
||||
Color bool
|
||||
}
|
||||
|
||||
// NewWarningPrinter returns an implementation of warningPrinter that outputs warnings to the specified writer.
|
||||
func NewWarningPrinter(out io.Writer, opts WarningPrinterOptions) *WarningPrinter {
|
||||
h := &WarningPrinter{out: out, opts: opts}
|
||||
return h
|
||||
}
|
||||
|
||||
// Print prints warnings to the configured writer.
|
||||
func (w *WarningPrinter) Print(message string) {
|
||||
if w.opts.Color {
|
||||
fmt.Fprintf(w.out, "%sWarning:%s %s\n", yellowColor, resetColor, message)
|
||||
} else {
|
||||
fmt.Fprintf(w.out, "Warning: %s\n", message)
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
|
||||
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 printers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// YAMLPrinter is an implementation of ResourcePrinter which outputs an object as YAML.
|
||||
// The input object is assumed to be in the internal version of an API and is converted
|
||||
// to the given version first.
|
||||
// If PrintObj() is called multiple times, objects are separated with a '---' separator.
|
||||
type YAMLPrinter struct {
|
||||
printCount int64
|
||||
}
|
||||
|
||||
// PrintObj prints the data as YAML.
|
||||
func (p *YAMLPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
|
||||
// we use reflect.Indirect here in order to obtain the actual value from a pointer.
|
||||
// we need an actual value in order to retrieve the package path for an object.
|
||||
// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
|
||||
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
|
||||
return errors.New(InternalObjectPrinterErr)
|
||||
}
|
||||
|
||||
count := atomic.AddInt64(&p.printCount, 1)
|
||||
if count > 1 {
|
||||
if _, err := w.Write([]byte("---\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch obj := obj.(type) {
|
||||
case *metav1.WatchEvent:
|
||||
if InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj.Object.Object)).Type().PkgPath()) {
|
||||
return errors.New(InternalObjectPrinterErr)
|
||||
}
|
||||
data, err := yaml.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(data)
|
||||
return err
|
||||
case *runtime.Unknown:
|
||||
data, err := yaml.JSONToYAML(obj.Raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
if obj.GetObjectKind().GroupVersionKind().Empty() {
|
||||
return fmt.Errorf("missing apiVersion or kind; try GetObjectKind().SetGroupVersionKind() if you know the type")
|
||||
}
|
||||
|
||||
output, err := yaml.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprint(w, string(output))
|
||||
return err
|
||||
}
|
||||
+1263
File diff suppressed because it is too large
Load Diff
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// TODO require negotiatedSerializer. leaving it optional lets us plumb current behavior and deal with the difference after major plumbing is complete
|
||||
func (clientConfigFn ClientConfigFunc) clientForGroupVersion(gv schema.GroupVersion, negotiatedSerializer runtime.NegotiatedSerializer) (RESTClient, error) {
|
||||
cfg, err := clientConfigFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if negotiatedSerializer != nil {
|
||||
cfg.ContentConfig.NegotiatedSerializer = negotiatedSerializer
|
||||
}
|
||||
cfg.GroupVersion = &gv
|
||||
if len(gv.Group) == 0 {
|
||||
cfg.APIPath = "/api"
|
||||
} else {
|
||||
cfg.APIPath = "/apis"
|
||||
}
|
||||
|
||||
return rest.RESTClientFor(cfg)
|
||||
}
|
||||
|
||||
func (clientConfigFn ClientConfigFunc) unstructuredClientForGroupVersion(gv schema.GroupVersion) (RESTClient, error) {
|
||||
cfg, err := clientConfigFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.ContentConfig = UnstructuredPlusDefaultContentConfig()
|
||||
cfg.GroupVersion = &gv
|
||||
if len(gv.Group) == 0 {
|
||||
cfg.APIPath = "/api"
|
||||
} else {
|
||||
cfg.APIPath = "/apis"
|
||||
}
|
||||
|
||||
return rest.RESTClientFor(cfg)
|
||||
}
|
||||
|
||||
func (clientConfigFn ClientConfigFunc) withStdinUnavailable(stdinUnavailable bool) ClientConfigFunc {
|
||||
return func() (*rest.Config, error) {
|
||||
cfg, err := clientConfigFn()
|
||||
if stdinUnavailable && cfg != nil && cfg.ExecProvider != nil {
|
||||
cfg.ExecProvider.StdinUnavailable = stdinUnavailable
|
||||
cfg.ExecProvider.StdinUnavailableMessage = "used by stdin resource manifest reader"
|
||||
}
|
||||
return cfg, err
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
// CRDGetter is a function that can download the list of GVK for all
|
||||
// CRDs.
|
||||
type CRDGetter func() ([]schema.GroupKind, error)
|
||||
|
||||
func CRDFromDynamic(client dynamic.Interface) CRDGetter {
|
||||
return func() ([]schema.GroupKind, error) {
|
||||
list, err := client.Resource(schema.GroupVersionResource{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Resource: "customresourcedefinitions",
|
||||
}).List(context.TODO(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list CRDs: %v", err)
|
||||
}
|
||||
if list == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gks := []schema.GroupKind{}
|
||||
|
||||
// We need to parse the list to get the gvk, I guess that's fine.
|
||||
for _, crd := range (*list).Items {
|
||||
// Look for group, version, and kind
|
||||
group, _, _ := unstructured.NestedString(crd.Object, "spec", "group")
|
||||
kind, _, _ := unstructured.NestedString(crd.Object, "spec", "names", "kind")
|
||||
|
||||
gks = append(gks, schema.GroupKind{
|
||||
Group: group,
|
||||
Kind: kind,
|
||||
})
|
||||
}
|
||||
|
||||
return gks, nil
|
||||
}
|
||||
}
|
||||
|
||||
// CRDFinder keeps a cache of known CRDs and finds a given GVK in the
|
||||
// list.
|
||||
type CRDFinder interface {
|
||||
HasCRD(gvk schema.GroupKind) (bool, error)
|
||||
}
|
||||
|
||||
func NewCRDFinder(getter CRDGetter) CRDFinder {
|
||||
return &crdFinder{
|
||||
getter: getter,
|
||||
}
|
||||
}
|
||||
|
||||
type crdFinder struct {
|
||||
getter CRDGetter
|
||||
cache *[]schema.GroupKind
|
||||
}
|
||||
|
||||
func (f *crdFinder) cacheCRDs() error {
|
||||
if f.cache != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
list, err := f.getter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.cache = &list
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *crdFinder) findCRD(gvk schema.GroupKind) bool {
|
||||
for _, crd := range *f.cache {
|
||||
if reflect.DeepEqual(gvk, crd) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *crdFinder) HasCRD(gvk schema.GroupKind) (bool, error) {
|
||||
if err := f.cacheCRDs(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return f.findCRD(gvk), nil
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
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
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
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 (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/restmapper"
|
||||
)
|
||||
|
||||
// FakeCategoryExpander is for testing only
|
||||
var FakeCategoryExpander restmapper.CategoryExpander = restmapper.SimpleCategoryExpander{
|
||||
Expansions: map[string][]schema.GroupResource{
|
||||
"all": {
|
||||
{Group: "", Resource: "pods"},
|
||||
{Group: "", Resource: "replicationcontrollers"},
|
||||
{Group: "", Resource: "services"},
|
||||
{Group: "apps", Resource: "statefulsets"},
|
||||
{Group: "autoscaling", Resource: "horizontalpodautoscalers"},
|
||||
{Group: "batch", Resource: "jobs"},
|
||||
{Group: "batch", Resource: "cronjobs"},
|
||||
{Group: "extensions", Resource: "daemonsets"},
|
||||
{Group: "extensions", Resource: "deployments"},
|
||||
{Group: "extensions", Resource: "replicasets"},
|
||||
},
|
||||
},
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
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 (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// fallbackQueryParamVerifier encapsulates the primary Verifier that
|
||||
// is invoked, and the secondary/fallback Verifier.
|
||||
type fallbackQueryParamVerifier struct {
|
||||
primary Verifier
|
||||
secondary Verifier
|
||||
}
|
||||
|
||||
var _ Verifier = &fallbackQueryParamVerifier{}
|
||||
|
||||
// NewFallbackQueryParamVerifier returns a new Verifier which will invoke the
|
||||
// initial/primary Verifier. If the primary Verifier is "NotFound", then the
|
||||
// secondary Verifier is invoked as a fallback.
|
||||
func NewFallbackQueryParamVerifier(primary Verifier, secondary Verifier) Verifier {
|
||||
return &fallbackQueryParamVerifier{
|
||||
primary: primary,
|
||||
secondary: secondary,
|
||||
}
|
||||
}
|
||||
|
||||
// HasSupport returns an error if the passed GVK does not support the
|
||||
// query param (fieldValidation), as determined by the primary and
|
||||
// secondary OpenAPI endpoints. The primary endoint is checked first,
|
||||
// but if there is an error retrieving the OpenAPI V3 document, the
|
||||
// secondary attempts to determine support. If the GVK supports the query param,
|
||||
// nil is returned.
|
||||
func (f *fallbackQueryParamVerifier) HasSupport(gvk schema.GroupVersionKind) error {
|
||||
err := f.primary.HasSupport(gvk)
|
||||
// If an error was returned from the primary OpenAPI endpoint,
|
||||
// we fallback to check the secondary OpenAPI endpoint for
|
||||
// any error *except* "paramUnsupportedError".
|
||||
if err != nil && !IsParamUnsupportedError(err) {
|
||||
klog.V(7).Infof("openapi v3 error...falling back to legacy: %s", err)
|
||||
err = f.secondary.HasSupport(gvk)
|
||||
}
|
||||
return err
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
var metadataAccessor = meta.NewAccessor()
|
||||
|
||||
// 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
|
||||
// The name of the subresource as the server would recognize it
|
||||
Subresource string
|
||||
// A RESTClient capable of mutating this resource.
|
||||
RESTClient RESTClient
|
||||
// True if the resource type is scoped to namespaces
|
||||
NamespaceScoped bool
|
||||
// If true, then use server-side dry-run to not persist changes to storage
|
||||
// for verbs and resources that support server-side dry-run.
|
||||
//
|
||||
// Note this should only be used against an apiserver with dry-run enabled,
|
||||
// and on resources that support dry-run. If the apiserver or the resource
|
||||
// does not support dry-run, then the change will be persisted to storage.
|
||||
ServerDryRun bool
|
||||
|
||||
// FieldManager is the name associated with the actor or entity that is making
|
||||
// changes.
|
||||
FieldManager string
|
||||
|
||||
// FieldValidation is the directive used to indicate how the server should perform
|
||||
// field validation (Ignore, Warn, or Strict)
|
||||
FieldValidation string
|
||||
}
|
||||
|
||||
// NewHelper creates a Helper from a ResourceMapping
|
||||
func NewHelper(client RESTClient, mapping *meta.RESTMapping) *Helper {
|
||||
return &Helper{
|
||||
Resource: mapping.Resource.Resource,
|
||||
RESTClient: client,
|
||||
NamespaceScoped: mapping.Scope.Name() == meta.RESTScopeNameNamespace,
|
||||
}
|
||||
}
|
||||
|
||||
// DryRun, if true, will use server-side dry-run to not persist changes to storage.
|
||||
// Otherwise, changes will be persisted to storage.
|
||||
func (m *Helper) DryRun(dryRun bool) *Helper {
|
||||
m.ServerDryRun = dryRun
|
||||
return m
|
||||
}
|
||||
|
||||
// WithFieldManager sets the field manager option to indicate the actor or entity
|
||||
// that is making changes in a create or update operation.
|
||||
func (m *Helper) WithFieldManager(fieldManager string) *Helper {
|
||||
m.FieldManager = fieldManager
|
||||
return m
|
||||
}
|
||||
|
||||
// WithFieldValidation sets the field validation option to indicate
|
||||
// how the server should perform field validation (Ignore, Warn, or Strict).
|
||||
func (m *Helper) WithFieldValidation(validationDirective string) *Helper {
|
||||
m.FieldValidation = validationDirective
|
||||
return m
|
||||
}
|
||||
|
||||
// Subresource sets the helper to access (<resource>/[ns/<namespace>/]<name>/<subresource>)
|
||||
func (m *Helper) WithSubresource(subresource string) *Helper {
|
||||
m.Subresource = subresource
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Helper) Get(namespace, name string) (runtime.Object, error) {
|
||||
req := m.RESTClient.Get().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
Name(name).
|
||||
SubResource(m.Subresource)
|
||||
return req.Do(context.TODO()).Get()
|
||||
}
|
||||
|
||||
func (m *Helper) List(namespace, apiVersion string, options *metav1.ListOptions) (runtime.Object, error) {
|
||||
req := m.RESTClient.Get().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
VersionedParams(options, metav1.ParameterCodec)
|
||||
return req.Do(context.TODO()).Get()
|
||||
}
|
||||
|
||||
// FollowContinue handles the continue parameter returned by the API server when using list
|
||||
// chunking. To take advantage of this, the initial ListOptions provided by the consumer
|
||||
// should include a non-zero Limit parameter.
|
||||
func FollowContinue(initialOpts *metav1.ListOptions,
|
||||
listFunc func(metav1.ListOptions) (runtime.Object, error)) error {
|
||||
opts := initialOpts
|
||||
for {
|
||||
list, err := listFunc(*opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextContinueToken, _ := metadataAccessor.Continue(list)
|
||||
if len(nextContinueToken) == 0 {
|
||||
return nil
|
||||
}
|
||||
opts.Continue = nextContinueToken
|
||||
}
|
||||
}
|
||||
|
||||
// EnhanceListError augments errors typically returned by List operations with additional context,
|
||||
// making sure to retain the StatusError type when applicable.
|
||||
func EnhanceListError(err error, opts metav1.ListOptions, subj string) error {
|
||||
if apierrors.IsResourceExpired(err) {
|
||||
return err
|
||||
}
|
||||
if apierrors.IsBadRequest(err) || apierrors.IsNotFound(err) {
|
||||
if se, ok := err.(*apierrors.StatusError); ok {
|
||||
// modify the message without hiding this is an API error
|
||||
if len(opts.LabelSelector) == 0 && len(opts.FieldSelector) == 0 {
|
||||
se.ErrStatus.Message = fmt.Sprintf("Unable to list %q: %v", subj,
|
||||
se.ErrStatus.Message)
|
||||
} else {
|
||||
se.ErrStatus.Message = fmt.Sprintf(
|
||||
"Unable to find %q that match label selector %q, field selector %q: %v", subj,
|
||||
opts.LabelSelector,
|
||||
opts.FieldSelector, se.ErrStatus.Message)
|
||||
}
|
||||
return se
|
||||
}
|
||||
if len(opts.LabelSelector) == 0 && len(opts.FieldSelector) == 0 {
|
||||
return fmt.Errorf("Unable to list %q: %v", subj, err)
|
||||
}
|
||||
return fmt.Errorf("Unable to find %q that match label selector %q, field selector %q: %v",
|
||||
subj, opts.LabelSelector, opts.FieldSelector, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Helper) Watch(namespace, apiVersion string, options *metav1.ListOptions) (watch.Interface, error) {
|
||||
options.Watch = true
|
||||
return m.RESTClient.Get().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
VersionedParams(options, metav1.ParameterCodec).
|
||||
Watch(context.TODO())
|
||||
}
|
||||
|
||||
func (m *Helper) WatchSingle(namespace, name, resourceVersion string) (watch.Interface, error) {
|
||||
return m.RESTClient.Get().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
VersionedParams(&metav1.ListOptions{
|
||||
ResourceVersion: resourceVersion,
|
||||
Watch: true,
|
||||
FieldSelector: fields.OneTermEqualSelector("metadata.name", name).String(),
|
||||
}, metav1.ParameterCodec).
|
||||
Watch(context.TODO())
|
||||
}
|
||||
|
||||
func (m *Helper) Delete(namespace, name string) (runtime.Object, error) {
|
||||
return m.DeleteWithOptions(namespace, name, nil)
|
||||
}
|
||||
|
||||
func (m *Helper) DeleteWithOptions(namespace, name string, options *metav1.DeleteOptions) (runtime.Object, error) {
|
||||
if options == nil {
|
||||
options = &metav1.DeleteOptions{}
|
||||
}
|
||||
if m.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
|
||||
return m.RESTClient.Delete().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
Name(name).
|
||||
Body(options).
|
||||
Do(context.TODO()).
|
||||
Get()
|
||||
}
|
||||
|
||||
func (m *Helper) Create(namespace string, modify bool, obj runtime.Object) (runtime.Object, error) {
|
||||
return m.CreateWithOptions(namespace, modify, obj, nil)
|
||||
}
|
||||
|
||||
func (m *Helper) CreateWithOptions(namespace string, modify bool, obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
if options == nil {
|
||||
options = &metav1.CreateOptions{}
|
||||
}
|
||||
if m.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
if m.FieldManager != "" {
|
||||
options.FieldManager = m.FieldManager
|
||||
}
|
||||
if m.FieldValidation != "" {
|
||||
options.FieldValidation = m.FieldValidation
|
||||
}
|
||||
if modify {
|
||||
// Attempt to version the object based on client logic.
|
||||
version, err := metadataAccessor.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 m.createResource(m.RESTClient, m.Resource, namespace, obj, options)
|
||||
}
|
||||
if version != "" {
|
||||
if err := metadataAccessor.SetResourceVersion(obj, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m.createResource(m.RESTClient, m.Resource, namespace, obj, options)
|
||||
}
|
||||
|
||||
func (m *Helper) createResource(c RESTClient, resource, namespace string, obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
return c.Post().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(resource).
|
||||
VersionedParams(options, metav1.ParameterCodec).
|
||||
Body(obj).
|
||||
Do(context.TODO()).
|
||||
Get()
|
||||
}
|
||||
func (m *Helper) Patch(namespace, name string, pt types.PatchType, data []byte, options *metav1.PatchOptions) (runtime.Object, error) {
|
||||
if options == nil {
|
||||
options = &metav1.PatchOptions{}
|
||||
}
|
||||
if m.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
if m.FieldManager != "" {
|
||||
options.FieldManager = m.FieldManager
|
||||
}
|
||||
if m.FieldValidation != "" {
|
||||
options.FieldValidation = m.FieldValidation
|
||||
}
|
||||
return m.RESTClient.Patch(pt).
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(m.Resource).
|
||||
Name(name).
|
||||
SubResource(m.Subresource).
|
||||
VersionedParams(options, metav1.ParameterCodec).
|
||||
Body(data).
|
||||
Do(context.TODO()).
|
||||
Get()
|
||||
}
|
||||
|
||||
func (m *Helper) Replace(namespace, name string, overwrite bool, obj runtime.Object) (runtime.Object, error) {
|
||||
c := m.RESTClient
|
||||
var options = &metav1.UpdateOptions{}
|
||||
if m.ServerDryRun {
|
||||
options.DryRun = []string{metav1.DryRunAll}
|
||||
}
|
||||
if m.FieldManager != "" {
|
||||
options.FieldManager = m.FieldManager
|
||||
}
|
||||
if m.FieldValidation != "" {
|
||||
options.FieldValidation = m.FieldValidation
|
||||
}
|
||||
|
||||
// Attempt to version the object based on client logic.
|
||||
version, err := metadataAccessor.ResourceVersion(obj)
|
||||
if err != nil {
|
||||
// We don't know how to version this object, so send it to the server as is
|
||||
return m.replaceResource(c, m.Resource, namespace, name, obj, options)
|
||||
}
|
||||
if version == "" && overwrite {
|
||||
// Retrieve the current version of the object to overwrite the server object
|
||||
serverObj, err := c.Get().NamespaceIfScoped(namespace, m.NamespaceScoped).Resource(m.Resource).Name(name).SubResource(m.Subresource).Do(context.TODO()).Get()
|
||||
if err != nil {
|
||||
// The object does not exist, but we want it to be created
|
||||
return m.replaceResource(c, m.Resource, namespace, name, obj, options)
|
||||
}
|
||||
serverVersion, err := metadataAccessor.ResourceVersion(serverObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := metadataAccessor.SetResourceVersion(obj, serverVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return m.replaceResource(c, m.Resource, namespace, name, obj, options)
|
||||
}
|
||||
|
||||
func (m *Helper) replaceResource(c RESTClient, resource, namespace, name string, obj runtime.Object, options *metav1.UpdateOptions) (runtime.Object, error) {
|
||||
return c.Put().
|
||||
NamespaceIfScoped(namespace, m.NamespaceScoped).
|
||||
Resource(resource).
|
||||
Name(name).
|
||||
SubResource(m.Subresource).
|
||||
VersionedParams(options, metav1.ParameterCodec).
|
||||
Body(obj).
|
||||
Do(context.TODO()).
|
||||
Get()
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
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 (
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
)
|
||||
|
||||
type RESTClientGetter interface {
|
||||
ToRESTConfig() (*rest.Config, error)
|
||||
ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
|
||||
ToRESTMapper() (meta.RESTMapper, error)
|
||||
}
|
||||
|
||||
type ClientConfigFunc func() (*rest.Config, error)
|
||||
type RESTMapperFunc func() (meta.RESTMapper, error)
|
||||
type CategoryExpanderFunc func() (restmapper.CategoryExpander, error)
|
||||
|
||||
// RESTClient is a client helper for dealing with RESTful resources
|
||||
// in a generic way.
|
||||
type RESTClient interface {
|
||||
Get() *rest.Request
|
||||
Post() *rest.Request
|
||||
Patch(types.PatchType) *rest.Request
|
||||
Delete() *rest.Request
|
||||
Put() *rest.Request
|
||||
}
|
||||
|
||||
// RequestTransform is a function that is given a chance to modify the outgoing request.
|
||||
type RequestTransform func(*rest.Request)
|
||||
|
||||
// NewClientWithOptions wraps the provided RESTClient and invokes each transform on each
|
||||
// newly created request.
|
||||
func NewClientWithOptions(c RESTClient, transforms ...RequestTransform) RESTClient {
|
||||
if len(transforms) == 0 {
|
||||
return c
|
||||
}
|
||||
return &clientOptions{c: c, transforms: transforms}
|
||||
}
|
||||
|
||||
type clientOptions struct {
|
||||
c RESTClient
|
||||
transforms []RequestTransform
|
||||
}
|
||||
|
||||
func (c *clientOptions) modify(req *rest.Request) *rest.Request {
|
||||
for _, transform := range c.transforms {
|
||||
transform(req)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func (c *clientOptions) Get() *rest.Request {
|
||||
return c.modify(c.c.Get())
|
||||
}
|
||||
|
||||
func (c *clientOptions) Post() *rest.Request {
|
||||
return c.modify(c.c.Post())
|
||||
}
|
||||
func (c *clientOptions) Patch(t types.PatchType) *rest.Request {
|
||||
return c.modify(c.c.Patch(t))
|
||||
}
|
||||
func (c *clientOptions) Delete() *rest.Request {
|
||||
return c.modify(c.c.Delete())
|
||||
}
|
||||
func (c *clientOptions) Put() *rest.Request {
|
||||
return c.modify(c.c.Put())
|
||||
}
|
||||
|
||||
// ContentValidator is an interface that knows how to validate an API object serialized to a byte array.
|
||||
type ContentValidator interface {
|
||||
ValidateBytes(data []byte) error
|
||||
}
|
||||
|
||||
// Visitor lets clients walk a list of resources.
|
||||
type Visitor interface {
|
||||
Visit(VisitorFunc) error
|
||||
}
|
||||
|
||||
// VisitorFunc implements the Visitor interface for a matching function.
|
||||
// If there was a problem walking a list of resources, the incoming error
|
||||
// will describe the problem and the function can decide how to handle that error.
|
||||
// A nil returned indicates to accept an error to continue loops even when errors happen.
|
||||
// This is useful for ignoring certain kinds of errors or aggregating errors in some way.
|
||||
type VisitorFunc func(*Info, error) error
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
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"
|
||||
|
||||
"sigs.k8s.io/kustomize/api/krusty"
|
||||
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||
)
|
||||
|
||||
// KustomizeVisitor handles kustomization.yaml files.
|
||||
type KustomizeVisitor struct {
|
||||
mapper *mapper
|
||||
schema ContentValidator
|
||||
// Directory expected to contain a kustomization file.
|
||||
dirPath string
|
||||
// File system containing dirPath.
|
||||
fSys filesys.FileSystem
|
||||
// Holds result of kustomize build, retained for tests.
|
||||
yml []byte
|
||||
}
|
||||
|
||||
// Visit passes the result of a kustomize build to a StreamVisitor.
|
||||
func (v *KustomizeVisitor) Visit(fn VisitorFunc) error {
|
||||
kOpts := krusty.MakeDefaultOptions()
|
||||
kOpts.Reorder = krusty.ReorderOptionLegacy
|
||||
k := krusty.MakeKustomizer(kOpts)
|
||||
m, err := k.Run(v.fSys, v.dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.yml, err = m.AsYaml()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sv := NewStreamVisitor(
|
||||
bytes.NewReader(v.yml), v.mapper, v.dirPath, v.schema)
|
||||
return sv.Visit(fn)
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
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"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// Mapper is a convenience struct for holding references to the interfaces
|
||||
// needed to create Info for arbitrary objects.
|
||||
type mapper struct {
|
||||
// localFn indicates the call can't make server requests
|
||||
localFn func() bool
|
||||
|
||||
restMapperFn RESTMapperFunc
|
||||
clientFn func(version schema.GroupVersion) (RESTClient, error)
|
||||
decoder runtime.Decoder
|
||||
}
|
||||
|
||||
// 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) {
|
||||
obj, gvk, err := m.decoder.Decode(data, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode %q: %v", source, err)
|
||||
}
|
||||
|
||||
name, _ := metadataAccessor.Name(obj)
|
||||
namespace, _ := metadataAccessor.Namespace(obj)
|
||||
resourceVersion, _ := metadataAccessor.ResourceVersion(obj)
|
||||
|
||||
ret := &Info{
|
||||
Source: source,
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
ResourceVersion: resourceVersion,
|
||||
|
||||
Object: obj,
|
||||
}
|
||||
|
||||
if m.localFn == nil || !m.localFn() {
|
||||
restMapper, err := m.restMapperFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||
if err != nil {
|
||||
if _, ok := err.(*meta.NoKindMatchError); ok {
|
||||
return nil, fmt.Errorf("resource mapping not found for name: %q namespace: %q from %q: %w\nensure CRDs are installed first",
|
||||
name, namespace, source, err)
|
||||
}
|
||||
return nil, fmt.Errorf("unable to recognize %q: %v", source, err)
|
||||
}
|
||||
ret.Mapping = mapping
|
||||
|
||||
client, err := m.clientFn(gvk.GroupVersion())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to connect to a server to handle %q: %v", mapping.Resource, err)
|
||||
}
|
||||
ret.Client = client
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// InfoForObject 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, typer runtime.ObjectTyper, preferredGVKs []schema.GroupVersionKind) (*Info, error) {
|
||||
groupVersionKinds, _, err := typer.ObjectKinds(obj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get type info from the object %q: %v", reflect.TypeOf(obj), err)
|
||||
}
|
||||
|
||||
gvk := groupVersionKinds[0]
|
||||
if len(groupVersionKinds) > 1 && len(preferredGVKs) > 0 {
|
||||
gvk = preferredObjectKind(groupVersionKinds, preferredGVKs)
|
||||
}
|
||||
|
||||
name, _ := metadataAccessor.Name(obj)
|
||||
namespace, _ := metadataAccessor.Namespace(obj)
|
||||
resourceVersion, _ := metadataAccessor.ResourceVersion(obj)
|
||||
ret := &Info{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
ResourceVersion: resourceVersion,
|
||||
|
||||
Object: obj,
|
||||
}
|
||||
|
||||
if m.localFn == nil || !m.localFn() {
|
||||
restMapper, err := m.restMapperFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to recognize %v", err)
|
||||
}
|
||||
ret.Mapping = mapping
|
||||
|
||||
client, err := m.clientFn(gvk.GroupVersion())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to connect to a server to handle %q: %v", mapping.Resource, err)
|
||||
}
|
||||
ret.Client = client
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// preferredObjectKind picks the possibility that most closely matches the priority list in this order:
|
||||
// GroupVersionKind matches (exact match)
|
||||
// GroupKind matches
|
||||
// Group matches
|
||||
func preferredObjectKind(possibilities []schema.GroupVersionKind, preferences []schema.GroupVersionKind) schema.GroupVersionKind {
|
||||
// Exact match
|
||||
for _, priority := range preferences {
|
||||
for _, possibility := range possibilities {
|
||||
if possibility == priority {
|
||||
return possibility
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GroupKind match
|
||||
for _, priority := range preferences {
|
||||
for _, possibility := range possibilities {
|
||||
if possibility.GroupKind() == priority.GroupKind() {
|
||||
return possibility
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group match
|
||||
for _, priority := range preferences {
|
||||
for _, possibility := range possibilities {
|
||||
if possibility.Group == priority.Group {
|
||||
return possibility
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Just pick the first
|
||||
return possibilities[0]
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
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 (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utiljson "k8s.io/apimachinery/pkg/util/json"
|
||||
)
|
||||
|
||||
// metadataValidatingDecoder wraps a decoder and additionally ensures metadata schema fields decode before returning an unstructured object
|
||||
type metadataValidatingDecoder struct {
|
||||
decoder runtime.Decoder
|
||||
}
|
||||
|
||||
func (m *metadataValidatingDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
|
||||
obj, gvk, err := m.decoder.Decode(data, defaults, into)
|
||||
|
||||
// if we already errored, return
|
||||
if err != nil {
|
||||
return obj, gvk, err
|
||||
}
|
||||
|
||||
// if we're not unstructured, return
|
||||
if _, isUnstructured := obj.(runtime.Unstructured); !isUnstructured {
|
||||
return obj, gvk, err
|
||||
}
|
||||
|
||||
// make sure the data can decode into ObjectMeta before we return,
|
||||
// so we don't silently truncate schema errors in metadata later with accesser get/set calls
|
||||
v := &metadataOnlyObject{}
|
||||
if typedErr := utiljson.Unmarshal(data, v); typedErr != nil {
|
||||
return obj, gvk, typedErr
|
||||
}
|
||||
return obj, gvk, err
|
||||
}
|
||||
|
||||
type metadataOnlyObject struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
openapi_v2 "github.com/google/gnostic-models/openapiv2"
|
||||
|
||||
yaml "go.yaml.in/yaml/v2"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
func NewQueryParamVerifier(dynamicClient dynamic.Interface, openAPIGetter discovery.OpenAPISchemaInterface, queryParam VerifiableQueryParam) *QueryParamVerifier {
|
||||
return &QueryParamVerifier{
|
||||
finder: NewCRDFinder(CRDFromDynamic(dynamicClient)),
|
||||
openAPIGetter: openAPIGetter,
|
||||
queryParam: queryParam,
|
||||
}
|
||||
}
|
||||
|
||||
// QueryParamVerifier verifies if a given group-version-kind supports a
|
||||
// given VerifiableQueryParam against the current server.
|
||||
//
|
||||
// Currently supported query params are: fieldValidation
|
||||
//
|
||||
// Support for each of these query params needs to be verified because
|
||||
// we determine whether or not to perform server-side or client-side
|
||||
// schema validation based on whether the fieldValidation query param is
|
||||
// supported or not.
|
||||
//
|
||||
// It reads the OpenAPI to see if the given GVK supports the given query param.
|
||||
// If the GVK can not be found, we assume that CRDs will have the same level of
|
||||
// support as "namespaces", and non-CRDs will not be supported. We
|
||||
// delay the check for CRDs as much as possible though, since it
|
||||
// requires an extra round-trip to the server.
|
||||
type QueryParamVerifier struct {
|
||||
finder CRDFinder
|
||||
openAPIGetter discovery.OpenAPISchemaInterface
|
||||
queryParam VerifiableQueryParam
|
||||
}
|
||||
|
||||
// Verifier is the generic verifier interface used for testing QueryParamVerifier
|
||||
type Verifier interface {
|
||||
HasSupport(gvk schema.GroupVersionKind) error
|
||||
}
|
||||
|
||||
// VerifiableQueryParam is a query parameter who's enablement on the
|
||||
// apiserver can be determined by evaluating the OpenAPI for a specific
|
||||
// GVK.
|
||||
type VerifiableQueryParam string
|
||||
|
||||
const (
|
||||
QueryParamFieldValidation VerifiableQueryParam = "fieldValidation"
|
||||
)
|
||||
|
||||
// HasSupport checks if the given gvk supports the query param configured on v
|
||||
func (v *QueryParamVerifier) HasSupport(gvk schema.GroupVersionKind) error {
|
||||
if (gvk == schema.GroupVersionKind{Version: "v1", Kind: "List"}) {
|
||||
return NewParamUnsupportedError(gvk, v.queryParam)
|
||||
}
|
||||
|
||||
oapi, err := v.openAPIGetter.OpenAPISchema()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download openapi: %v", err)
|
||||
}
|
||||
supports, err := supportsQueryParam(oapi, gvk, v.queryParam)
|
||||
if err != nil {
|
||||
// We assume that we couldn't find the type, then check for namespace:
|
||||
supports, _ = supportsQueryParam(oapi, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, v.queryParam)
|
||||
// If namespace supports the query param, then we will support the query param for CRDs only.
|
||||
if supports {
|
||||
supports, err = v.finder.HasCRD(gvk.GroupKind())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check CRD: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !supports {
|
||||
return NewParamUnsupportedError(gvk, v.queryParam)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type paramUnsupportedError struct {
|
||||
gvk schema.GroupVersionKind
|
||||
param VerifiableQueryParam
|
||||
}
|
||||
|
||||
func NewParamUnsupportedError(gvk schema.GroupVersionKind, param VerifiableQueryParam) error {
|
||||
return ¶mUnsupportedError{
|
||||
gvk: gvk,
|
||||
param: param,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *paramUnsupportedError) Error() string {
|
||||
return fmt.Sprintf("%v doesn't support %s", e.gvk, e.param)
|
||||
}
|
||||
|
||||
func IsParamUnsupportedError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := err.(*paramUnsupportedError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func hasGVKExtension(extensions []*openapi_v2.NamedAny, gvk schema.GroupVersionKind) bool {
|
||||
for _, extension := range extensions {
|
||||
if extension.GetValue().GetYaml() == "" ||
|
||||
extension.GetName() != "x-kubernetes-group-version-kind" {
|
||||
continue
|
||||
}
|
||||
var value map[string]string
|
||||
err := yaml.Unmarshal([]byte(extension.GetValue().GetYaml()), &value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if value["group"] == gvk.Group && value["kind"] == gvk.Kind && value["version"] == gvk.Version {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// supportsQueryParam is a method that let's us look in the OpenAPI if the
|
||||
// specific group-version-kind supports the specific query parameter for
|
||||
// the PATCH end-point.
|
||||
func supportsQueryParam(doc *openapi_v2.Document, gvk schema.GroupVersionKind, queryParam VerifiableQueryParam) (bool, error) {
|
||||
globalParams := map[string]*openapi_v2.NamedParameter{}
|
||||
for _, p := range doc.GetParameters().GetAdditionalProperties() {
|
||||
globalParams["#/parameters/"+p.GetName()] = p
|
||||
}
|
||||
|
||||
for _, path := range doc.GetPaths().GetPath() {
|
||||
// Is this describing the gvk we're looking for?
|
||||
if !hasGVKExtension(path.GetValue().GetPatch().GetVendorExtension(), gvk) {
|
||||
continue
|
||||
}
|
||||
for _, param := range path.GetValue().GetPatch().GetParameters() {
|
||||
if param.GetParameter().GetNonBodyParameter().GetQueryParameterSubSchema().GetName() == string(queryParam) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// lookup global parameters
|
||||
if ref := param.GetJsonReference().GetXRef(); ref != "" {
|
||||
if globalParam, ok := globalParams[ref]; ok && globalParam != nil && globalParam.GetValue().GetNonBodyParameter().GetQueryParameterSubSchema().GetName() == string(queryParam) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.New("couldn't find GVK in openapi")
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
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"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/openapi"
|
||||
"k8s.io/client-go/openapi3"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
||||
var _ Verifier = &queryParamVerifierV3{}
|
||||
|
||||
// NewQueryParamVerifierV3 returns a pointer to the created queryParamVerifier3 struct,
|
||||
// which implements the Verifier interface. The caching characteristics of the
|
||||
// OpenAPI V3 specs are determined by the passed oapiClient. For memory caching, the
|
||||
// client should be wrapped beforehand as: cached.NewClient(oapiClient). The disk
|
||||
// caching is determined by the discovery client the oapiClient is created from.
|
||||
func NewQueryParamVerifierV3(dynamicClient dynamic.Interface, oapiClient openapi.Client, queryParam VerifiableQueryParam) Verifier {
|
||||
return &queryParamVerifierV3{
|
||||
finder: NewCRDFinder(CRDFromDynamic(dynamicClient)),
|
||||
root: openapi3.NewRoot(oapiClient),
|
||||
queryParam: queryParam,
|
||||
}
|
||||
}
|
||||
|
||||
// queryParamVerifierV3 encapsulates info necessary to determine if
|
||||
// the queryParam is a parameter for the Patch endpoint for a
|
||||
// passed GVK.
|
||||
type queryParamVerifierV3 struct {
|
||||
finder CRDFinder
|
||||
root openapi3.Root
|
||||
queryParam VerifiableQueryParam
|
||||
}
|
||||
|
||||
var namespaceGVK = schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}
|
||||
|
||||
// HasSupport returns nil error if the passed GVK supports the parameter
|
||||
// (stored in struct; usually "fieldValidation") for Patch endpoint.
|
||||
// Returns an error if the passed GVK does not support the query param,
|
||||
// or if another error occurred. If the Open API V3 spec for a CRD is not
|
||||
// found, then the spec for Namespace is checked for query param support instead.
|
||||
func (v *queryParamVerifierV3) HasSupport(gvk schema.GroupVersionKind) error {
|
||||
if (gvk == schema.GroupVersionKind{Version: "v1", Kind: "List"}) {
|
||||
return NewParamUnsupportedError(gvk, v.queryParam)
|
||||
}
|
||||
gvSpec, err := v.root.GVSpec(gvk.GroupVersion())
|
||||
if err == nil {
|
||||
return supportsQueryParamV3(gvSpec, gvk, v.queryParam)
|
||||
}
|
||||
if _, isErr := err.(*openapi3.GroupVersionNotFoundError); !isErr {
|
||||
return err
|
||||
}
|
||||
// If the spec for the passed GVK is not found, then check if it is a CRD.
|
||||
// For CRD's substitute Namespace OpenAPI V3 spec to check if query param is supported.
|
||||
if found, _ := v.finder.HasCRD(gvk.GroupKind()); found {
|
||||
namespaceSpec, err := v.root.GVSpec(namespaceGVK.GroupVersion())
|
||||
if err != nil {
|
||||
// If error retrieving Namespace spec, propagate error.
|
||||
return err
|
||||
}
|
||||
return supportsQueryParamV3(namespaceSpec, namespaceGVK, v.queryParam)
|
||||
}
|
||||
return NewParamUnsupportedError(gvk, v.queryParam)
|
||||
}
|
||||
|
||||
// hasGVKExtensionV3 returns true if the passed OpenAPI extensions map contains
|
||||
// the passed GVK; false otherwise.
|
||||
func hasGVKExtensionV3(extensions spec.Extensions, gvk schema.GroupVersionKind) bool {
|
||||
var oapiGVK map[string]string
|
||||
err := extensions.GetObject("x-kubernetes-group-version-kind", &oapiGVK)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if oapiGVK["group"] == gvk.Group &&
|
||||
oapiGVK["version"] == gvk.Version &&
|
||||
oapiGVK["kind"] == gvk.Kind {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// supportsQueryParam is a method that let's us look in the OpenAPI if the
|
||||
// specific group-version-kind supports the specific query parameter for
|
||||
// the PATCH end-point. Returns nil if the passed GVK supports the passed
|
||||
// query parameter; otherwise, a "paramUnsupportedError" is returned (except
|
||||
// when an invalid document error is returned when an invalid OpenAPI V3
|
||||
// is passed in).
|
||||
func supportsQueryParamV3(doc *spec3.OpenAPI, gvk schema.GroupVersionKind, queryParam VerifiableQueryParam) error {
|
||||
if doc == nil || doc.Paths == nil {
|
||||
return fmt.Errorf("Invalid OpenAPI V3 document")
|
||||
}
|
||||
for _, path := range doc.Paths.Paths {
|
||||
// If operation is not PATCH, then continue.
|
||||
if path == nil {
|
||||
continue
|
||||
}
|
||||
op := path.PathProps.Patch
|
||||
if op == nil {
|
||||
continue
|
||||
}
|
||||
// Is this PATCH operation for the passed GVK?
|
||||
if !hasGVKExtensionV3(op.VendorExtensible.Extensions, gvk) {
|
||||
continue
|
||||
}
|
||||
// Now look for the query parameter among the parameters
|
||||
// for the PATCH operation.
|
||||
for _, param := range op.OperationProps.Parameters {
|
||||
if param.ParameterProps.Name == string(queryParam) && param.In == "query" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookup global parameters
|
||||
if ref := param.Refable.Ref.Ref.String(); strings.HasPrefix(ref, "#/parameters/") && doc.Components != nil {
|
||||
k := strings.TrimPrefix(ref, "#/parameters/")
|
||||
if globalParam, ok := doc.Components.Parameters[k]; ok && globalParam != nil {
|
||||
if globalParam.In == "query" && globalParam.Name == string(queryParam) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewParamUnsupportedError(gvk, queryParam)
|
||||
}
|
||||
return fmt.Errorf("Path not found for GVK (%s) in OpenAPI V3 doc", gvk)
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
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"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
// ErrMatchFunc can be used to filter errors that may not be true failures.
|
||||
type ErrMatchFunc func(error) bool
|
||||
|
||||
// Result contains helper methods for dealing with the outcome of a Builder.
|
||||
type Result struct {
|
||||
err error
|
||||
visitor Visitor
|
||||
|
||||
sources []Visitor
|
||||
singleItemImplied bool
|
||||
targetsSingleItems bool
|
||||
|
||||
mapper *mapper
|
||||
ignoreErrors []utilerrors.Matcher
|
||||
|
||||
// populated by a call to Infos
|
||||
info []*Info
|
||||
}
|
||||
|
||||
// withError allows a fluent style for internal result code.
|
||||
func (r *Result) withError(err error) *Result {
|
||||
r.err = err
|
||||
return r
|
||||
}
|
||||
|
||||
// TargetsSingleItems returns true if any of the builder arguments pointed
|
||||
// to non-list calls (if the user explicitly asked for any object by name).
|
||||
// This includes directories, streams, URLs, and resource name tuples.
|
||||
func (r *Result) TargetsSingleItems() bool {
|
||||
return r.targetsSingleItems
|
||||
}
|
||||
|
||||
// IgnoreErrors will filter errors that occur when by visiting the result
|
||||
// (but not errors that occur by creating the result in the first place),
|
||||
// eliminating any that match fns. This is best used in combination with
|
||||
// Builder.ContinueOnError(), where the visitors accumulate errors and return
|
||||
// them after visiting as a slice of errors. If no errors remain after
|
||||
// filtering, the various visitor methods on Result will return nil for
|
||||
// err.
|
||||
func (r *Result) IgnoreErrors(fns ...ErrMatchFunc) *Result {
|
||||
for _, fn := range fns {
|
||||
r.ignoreErrors = append(r.ignoreErrors, utilerrors.Matcher(fn))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Mapper returns a copy of the builder's mapper.
|
||||
func (r *Result) Mapper() *mapper {
|
||||
return r.mapper
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
err := r.visitor.Visit(fn)
|
||||
return utilerrors.FilterOut(err, r.ignoreErrors...)
|
||||
}
|
||||
|
||||
// IntoSingleItemImplied sets the provided boolean pointer to true if the Builder input
|
||||
// implies a single item, or multiple.
|
||||
func (r *Result) IntoSingleItemImplied(b *bool) *Result {
|
||||
*b = r.singleItemImplied
|
||||
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, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
infos = append(infos, info)
|
||||
return nil
|
||||
})
|
||||
err = utilerrors.FilterOut(err, r.ignoreErrors...)
|
||||
|
||||
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
|
||||
// v1.List. The ResourceVersion of the v1.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 := sets.New[string]()
|
||||
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.singleItemImplied {
|
||||
return objects[0], nil
|
||||
}
|
||||
// if the item is a list already, don't create another list
|
||||
if meta.IsListType(objects[0]) {
|
||||
return objects[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
version := ""
|
||||
if len(versions) == 1 {
|
||||
version = versions.UnsortedList()[0]
|
||||
}
|
||||
|
||||
return toV1List(objects, version), err
|
||||
}
|
||||
|
||||
// Compile time check to enforce that list implements the necessary interface
|
||||
var _ metav1.ListInterface = &v1.List{}
|
||||
var _ metav1.ListMetaAccessor = &v1.List{}
|
||||
|
||||
// toV1List takes a slice of Objects + their version, and returns
|
||||
// a v1.List Object containing the objects in the Items field
|
||||
func toV1List(objects []runtime.Object, version string) runtime.Object {
|
||||
raw := []runtime.RawExtension{}
|
||||
for _, o := range objects {
|
||||
raw = append(raw, runtime.RawExtension{Object: o})
|
||||
}
|
||||
return &v1.List{
|
||||
ListMeta: metav1.ListMeta{
|
||||
ResourceVersion: version,
|
||||
},
|
||||
Items: raw,
|
||||
}
|
||||
}
|
||||
|
||||
// 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[schema.GroupVersionResource]*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 individual resources and resource collections - %d resources were found", len(info))
|
||||
}
|
||||
return info[0].Watch(resourceVersion)
|
||||
}
|
||||
return w.Watch(resourceVersion)
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// dynamicCodec is a codec that wraps the standard unstructured codec
|
||||
// with special handling for Status objects.
|
||||
// Deprecated only used by test code and its wrong
|
||||
type dynamicCodec struct{}
|
||||
|
||||
func (dynamicCodec) Decode(data []byte, gvk *schema.GroupVersionKind, obj runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
|
||||
obj, gvk, err := unstructured.UnstructuredJSONScheme.Decode(data, gvk, obj)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if strings.EqualFold(gvk.Kind, "status") && gvk.Version == "v1" && (gvk.Group == "" || gvk.Group == "meta.k8s.io") {
|
||||
if _, ok := obj.(*metav1.Status); !ok {
|
||||
obj = &metav1.Status{}
|
||||
err := json.Unmarshal(data, obj)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj, gvk, nil
|
||||
}
|
||||
|
||||
func (dynamicCodec) Encode(obj runtime.Object, w io.Writer) error {
|
||||
// There is no need to handle runtime.CacheableObject, as we only
|
||||
// fallback to other encoders here.
|
||||
return unstructured.UnstructuredJSONScheme.Encode(obj, w)
|
||||
}
|
||||
|
||||
// Identifier implements runtime.Encoder interface.
|
||||
func (dynamicCodec) Identifier() runtime.Identifier {
|
||||
return unstructured.UnstructuredJSONScheme.Identifier()
|
||||
}
|
||||
|
||||
// UnstructuredPlusDefaultContentConfig returns a rest.ContentConfig for dynamic types. It includes enough codecs to act as a "normal"
|
||||
// serializer for the rest.client with options, status and the like.
|
||||
func UnstructuredPlusDefaultContentConfig() rest.ContentConfig {
|
||||
// TODO: scheme.Codecs here should become "pkg/apis/server/scheme" which is the minimal core you need
|
||||
// to talk to a kubernetes server
|
||||
jsonInfo, _ := runtime.SerializerInfoForMediaType(scheme.Codecs.SupportedMediaTypes(), runtime.ContentTypeJSON)
|
||||
|
||||
jsonInfo.Serializer = dynamicCodec{}
|
||||
jsonInfo.PrettySerializer = nil
|
||||
return rest.ContentConfig{
|
||||
AcceptContentTypes: runtime.ContentTypeJSON,
|
||||
ContentType: runtime.ContentTypeJSON,
|
||||
NegotiatedSerializer: serializer.NegotiatedSerializerWrapper(jsonInfo),
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
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 (
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
// Selector is a Visitor for resources that match a label selector.
|
||||
type Selector struct {
|
||||
Client RESTClient
|
||||
Mapping *meta.RESTMapping
|
||||
Namespace string
|
||||
LabelSelector string
|
||||
FieldSelector string
|
||||
LimitChunks int64
|
||||
}
|
||||
|
||||
// NewSelector creates a resource selector which hides details of getting items by their label selector.
|
||||
func NewSelector(client RESTClient, mapping *meta.RESTMapping, namespace, labelSelector, fieldSelector string, limitChunks int64) *Selector {
|
||||
return &Selector{
|
||||
Client: client,
|
||||
Mapping: mapping,
|
||||
Namespace: namespace,
|
||||
LabelSelector: labelSelector,
|
||||
FieldSelector: fieldSelector,
|
||||
LimitChunks: limitChunks,
|
||||
}
|
||||
}
|
||||
|
||||
// Visit implements Visitor and uses request chunking by default.
|
||||
func (r *Selector) Visit(fn VisitorFunc) error {
|
||||
helper := NewHelper(r.Client, r.Mapping)
|
||||
initialOpts := metav1.ListOptions{
|
||||
LabelSelector: r.LabelSelector,
|
||||
FieldSelector: r.FieldSelector,
|
||||
Limit: r.LimitChunks,
|
||||
}
|
||||
return FollowContinue(&initialOpts, func(options metav1.ListOptions) (runtime.Object, error) {
|
||||
list, err := helper.List(
|
||||
r.Namespace,
|
||||
r.ResourceMapping().GroupVersionKind.GroupVersion().String(),
|
||||
&options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, EnhanceListError(err, options, r.Mapping.Resource.String())
|
||||
}
|
||||
resourceVersion, _ := metadataAccessor.ResourceVersion(list)
|
||||
|
||||
info := &Info{
|
||||
Client: r.Client,
|
||||
Mapping: r.Mapping,
|
||||
|
||||
Namespace: r.Namespace,
|
||||
ResourceVersion: resourceVersion,
|
||||
|
||||
Object: list,
|
||||
}
|
||||
|
||||
if err := fn(info, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Selector) Watch(resourceVersion string) (watch.Interface, error) {
|
||||
return NewHelper(r.Client, r.Mapping).Watch(r.Namespace, r.ResourceMapping().GroupVersionKind.GroupVersion().String(),
|
||||
&metav1.ListOptions{ResourceVersion: resourceVersion, LabelSelector: r.LabelSelector, FieldSelector: r.FieldSelector})
|
||||
}
|
||||
|
||||
// ResourceMapping returns the mapping for this resource and implements ResourceMapping
|
||||
func (r *Selector) ResourceMapping() *meta.RESTMapping {
|
||||
return r.Mapping
|
||||
}
|
||||
+770
@@ -0,0 +1,770 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
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"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
const (
|
||||
constSTDINstr = "STDIN"
|
||||
stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false"
|
||||
)
|
||||
|
||||
// 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 will only be present if this builder was not local
|
||||
Client RESTClient
|
||||
// Mapping will only be present if this builder was not local
|
||||
Mapping *meta.RESTMapping
|
||||
|
||||
// Namespace will be set if the object is namespaced and has a specified value.
|
||||
Namespace string
|
||||
Name string
|
||||
|
||||
// Optional, Source is the filename or URL to template file (.json or .yaml),
|
||||
// or stdin to use to handle the resource
|
||||
Source string
|
||||
// Optional, this is the most recent value returned by the server if available. It will
|
||||
// typically be in unstructured or internal forms, depending on how the Builder was
|
||||
// defined. If retrieved from the server, the Builder expects the mapping client to
|
||||
// decide the final form. Use the AsVersioned, AsUnstructured, and AsInternal helpers
|
||||
// to alter the object versions.
|
||||
// If Subresource is specified, this will be the object for the subresource.
|
||||
Object 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
|
||||
// Optional, if specified, the object is the most recent value of the subresource
|
||||
// returned by the server if available.
|
||||
Subresource string
|
||||
}
|
||||
|
||||
// Visit implements Visitor
|
||||
func (i *Info) Visit(fn VisitorFunc) error {
|
||||
return fn(i, nil)
|
||||
}
|
||||
|
||||
// Get retrieves the object from the Namespace and Name fields
|
||||
func (i *Info) Get() (err error) {
|
||||
obj, err := NewHelper(i.Client, i.Mapping).WithSubresource(i.Subresource).Get(i.Namespace, i.Name)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) && len(i.Namespace) > 0 && i.Namespace != metav1.NamespaceDefault && i.Namespace != metav1.NamespaceAll {
|
||||
err2 := i.Client.Get().AbsPath("api", "v1", "namespaces", i.Namespace).Do(context.TODO()).Error()
|
||||
if err2 != nil && errors.IsNotFound(err2) {
|
||||
return err2
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
i.Object = obj
|
||||
i.ResourceVersion, _ = metadataAccessor.ResourceVersion(obj)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh updates the object with another object. If ignoreError is set
|
||||
// the Object will be updated even if name, namespace, or resourceVersion
|
||||
// attributes cannot be loaded from the object.
|
||||
func (i *Info) Refresh(obj runtime.Object, ignoreError bool) error {
|
||||
name, err := metadataAccessor.Name(obj)
|
||||
if err != nil {
|
||||
if !ignoreError {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
i.Name = name
|
||||
}
|
||||
namespace, err := metadataAccessor.Namespace(obj)
|
||||
if err != nil {
|
||||
if !ignoreError {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
i.Namespace = namespace
|
||||
}
|
||||
version, err := metadataAccessor.ResourceVersion(obj)
|
||||
if err != nil {
|
||||
if !ignoreError {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
i.ResourceVersion = version
|
||||
}
|
||||
i.Object = obj
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObjectName returns an approximate form of the resource's kind/name.
|
||||
func (i *Info) ObjectName() string {
|
||||
if i.Mapping != nil {
|
||||
return fmt.Sprintf("%s/%s", i.Mapping.Resource.Resource, i.Name)
|
||||
}
|
||||
gvk := i.Object.GetObjectKind().GroupVersionKind()
|
||||
if len(gvk.Group) == 0 {
|
||||
return fmt.Sprintf("%s/%s", strings.ToLower(gvk.Kind), i.Name)
|
||||
}
|
||||
return fmt.Sprintf("%s.%s/%s\n", strings.ToLower(gvk.Kind), gvk.Group, i.Name)
|
||||
}
|
||||
|
||||
// String returns the general purpose string representation
|
||||
func (i *Info) String() string {
|
||||
basicInfo := fmt.Sprintf("Name: %q, Namespace: %q", i.Name, i.Namespace)
|
||||
if i.Mapping != nil {
|
||||
mappingInfo := fmt.Sprintf("Resource: %q, GroupVersionKind: %q", i.Mapping.Resource.String(),
|
||||
i.Mapping.GroupVersionKind.String())
|
||||
return fmt.Sprint(mappingInfo, "\n", basicInfo)
|
||||
}
|
||||
return basicInfo
|
||||
}
|
||||
|
||||
// Namespaced returns true if the object belongs to a namespace
|
||||
func (i *Info) Namespaced() bool {
|
||||
if i.Mapping != nil {
|
||||
// if we have RESTMapper info, use it
|
||||
return i.Mapping.Scope.Name() == meta.RESTScopeNameNamespace
|
||||
}
|
||||
// otherwise, use the presence of a namespace in the info as an indicator
|
||||
return len(i.Namespace) > 0
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
type ConcurrentVisitorList struct {
|
||||
visitors []Visitor
|
||||
concurrency int
|
||||
}
|
||||
|
||||
func (l ConcurrentVisitorList) Visit(fn VisitorFunc) error {
|
||||
g := errgroup.Group{}
|
||||
|
||||
// Concurrency 1 just runs the visitors sequentially, this is the default
|
||||
// as it preserves the previous behavior, but allows components to opt into
|
||||
// concurrency.
|
||||
concurrency := 1
|
||||
if l.concurrency > concurrency {
|
||||
concurrency = l.concurrency
|
||||
}
|
||||
g.SetLimit(concurrency)
|
||||
|
||||
for i := range l.visitors {
|
||||
i := i
|
||||
g.Go(func() error {
|
||||
return l.visitors[i].Visit(fn)
|
||||
})
|
||||
}
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var errs []error
|
||||
for i := range l {
|
||||
err := l[i].Visit(func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return nil
|
||||
}
|
||||
if err := fn(info, nil); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
func ValidateSchema(data []byte, schema ContentValidator) error {
|
||||
if schema == nil {
|
||||
return nil
|
||||
}
|
||||
if err := schema.ValidateBytes(data); err != nil {
|
||||
return fmt.Errorf("error validating data: %v; %s", err, stopValidateMessage)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// URLVisitor downloads the contents of a URL, and if successful, returns
|
||||
// an info object representing the downloaded object.
|
||||
type URLVisitor struct {
|
||||
URL *url.URL
|
||||
*StreamVisitor
|
||||
HttpAttemptCount int
|
||||
}
|
||||
|
||||
func (v *URLVisitor) Visit(fn VisitorFunc) error {
|
||||
body, err := readHttpWithRetries(httpgetImpl, time.Second, v.URL.String(), v.HttpAttemptCount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer body.Close()
|
||||
v.StreamVisitor.Reader = body
|
||||
return v.StreamVisitor.Visit(fn)
|
||||
}
|
||||
|
||||
// readHttpWithRetries tries to http.Get the v.URL retries times before giving up.
|
||||
func readHttpWithRetries(get httpget, duration time.Duration, u string, attempts int) (io.ReadCloser, error) {
|
||||
var err error
|
||||
if attempts <= 0 {
|
||||
return nil, fmt.Errorf("http attempts must be greater than 0, was %d", attempts)
|
||||
}
|
||||
for i := 0; i < attempts; i++ {
|
||||
var (
|
||||
statusCode int
|
||||
status string
|
||||
body io.ReadCloser
|
||||
)
|
||||
if i > 0 {
|
||||
time.Sleep(duration)
|
||||
}
|
||||
|
||||
// Try to get the URL
|
||||
statusCode, status, body, err = get(u)
|
||||
|
||||
// Retry Errors
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if statusCode == http.StatusOK {
|
||||
return body, nil
|
||||
}
|
||||
body.Close()
|
||||
// Error - Set the error condition from the StatusCode
|
||||
err = fmt.Errorf("unable to read URL %q, server reported %s, status code=%d", u, status, statusCode)
|
||||
|
||||
if statusCode >= 500 && statusCode < 600 {
|
||||
// Retry 500's
|
||||
continue
|
||||
} else {
|
||||
// Don't retry other StatusCodes
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// httpget Defines function to retrieve a url and return the results. Exists for unit test stubbing.
|
||||
type httpget func(url string) (int, string, io.ReadCloser, error)
|
||||
|
||||
// httpgetImpl Implements a function to retrieve a url and return the results.
|
||||
func httpgetImpl(url string) (int, string, io.ReadCloser, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return 0, "", nil, err
|
||||
}
|
||||
return resp.StatusCode, resp.Status, resp.Body, nil
|
||||
}
|
||||
|
||||
// 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, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range v.decorators {
|
||||
if err := v.decorators[i](info, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fn(info, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// ContinueOnErrorVisitor visits each item and, if an error occurs on
|
||||
// any individual item, returns an aggregate error after all items
|
||||
// are visited.
|
||||
type ContinueOnErrorVisitor struct {
|
||||
Visitor
|
||||
}
|
||||
|
||||
// Visit returns nil if no error occurs during traversal, a regular
|
||||
// error if one occurs, or if multiple errors occur, an aggregate
|
||||
// error. If the provided visitor fails on any individual item it
|
||||
// will not prevent the remaining items from being visited. An error
|
||||
// returned by the visitor directly may still result in some items
|
||||
// not being visited.
|
||||
func (v ContinueOnErrorVisitor) Visit(fn VisitorFunc) error {
|
||||
var errs []error
|
||||
err := v.Visitor.Visit(func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return nil
|
||||
}
|
||||
if err := fn(info, nil); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if len(errs) == 1 {
|
||||
return errs[0]
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
// 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. Returns nil in the case of no errors.
|
||||
// When an error is hit on sub items (for instance, if a List contains an object that does
|
||||
// not have a registered client or resource), returns an aggregate error.
|
||||
type FlattenListVisitor struct {
|
||||
visitor Visitor
|
||||
typer runtime.ObjectTyper
|
||||
mapper *mapper
|
||||
}
|
||||
|
||||
// NewFlattenListVisitor creates a visitor that will expand list style runtime.Objects
|
||||
// into individual items and then visit them individually.
|
||||
func NewFlattenListVisitor(v Visitor, typer runtime.ObjectTyper, mapper *mapper) Visitor {
|
||||
return FlattenListVisitor{v, typer, mapper}
|
||||
}
|
||||
|
||||
func (v FlattenListVisitor) Visit(fn VisitorFunc) error {
|
||||
return v.visitor.Visit(func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Object == nil {
|
||||
return fn(info, nil)
|
||||
}
|
||||
if !meta.IsListType(info.Object) {
|
||||
return fn(info, nil)
|
||||
}
|
||||
|
||||
items := []runtime.Object{}
|
||||
itemsToProcess := []runtime.Object{info.Object}
|
||||
|
||||
for i := 0; i < len(itemsToProcess); i++ {
|
||||
currObj := itemsToProcess[i]
|
||||
if !meta.IsListType(currObj) {
|
||||
items = append(items, currObj)
|
||||
continue
|
||||
}
|
||||
|
||||
currItems, err := meta.ExtractList(currObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if errs := runtime.DecodeList(currItems, v.mapper.decoder); len(errs) > 0 {
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
itemsToProcess = append(itemsToProcess, currItems...)
|
||||
}
|
||||
|
||||
// If we have a GroupVersionKind on the list, prioritize that when asking for info on the objects contained in the list
|
||||
var preferredGVKs []schema.GroupVersionKind
|
||||
if info.Mapping != nil && !info.Mapping.GroupVersionKind.Empty() {
|
||||
preferredGVKs = append(preferredGVKs, info.Mapping.GroupVersionKind)
|
||||
}
|
||||
var errs []error
|
||||
for i := range items {
|
||||
item, err := v.mapper.infoForObject(items[i], v.typer, preferredGVKs)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
if len(info.ResourceVersion) != 0 {
|
||||
item.ResourceVersion = info.ResourceVersion
|
||||
}
|
||||
// propagate list source to items source
|
||||
if len(info.Source) != 0 {
|
||||
item.Source = info.Source
|
||||
}
|
||||
if err := fn(item, nil); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
})
|
||||
}
|
||||
|
||||
func ignoreFile(path string, extensions []string) bool {
|
||||
if len(extensions) == 0 {
|
||||
return false
|
||||
}
|
||||
ext := filepath.Ext(path)
|
||||
for _, s := range extensions {
|
||||
if s == ext {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// FileVisitorForSTDIN return a special FileVisitor just for STDIN
|
||||
func FileVisitorForSTDIN(mapper *mapper, schema ContentValidator) Visitor {
|
||||
return &FileVisitor{
|
||||
Path: constSTDINstr,
|
||||
StreamVisitor: NewStreamVisitor(nil, mapper, constSTDINstr, schema),
|
||||
}
|
||||
}
|
||||
|
||||
// ExpandPathsToFileVisitors will return a slice of FileVisitors that will handle files from the provided path.
|
||||
// After FileVisitors open the files, they will pass an io.Reader to a StreamVisitor to do the reading. (stdin
|
||||
// is also taken care of). Paths argument also accepts a single file, and will return a single visitor
|
||||
func ExpandPathsToFileVisitors(mapper *mapper, paths string, recursive bool, extensions []string, schema ContentValidator) ([]Visitor, error) {
|
||||
var visitors []Visitor
|
||||
err := filepath.Walk(paths, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
if path != paths && !recursive {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Don't check extension if the filepath was passed explicitly
|
||||
if path != paths && ignoreFile(path, extensions) {
|
||||
return nil
|
||||
}
|
||||
|
||||
visitor := &FileVisitor{
|
||||
Path: path,
|
||||
StreamVisitor: NewStreamVisitor(nil, mapper, path, schema),
|
||||
}
|
||||
|
||||
visitors = append(visitors, visitor)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return visitors, nil
|
||||
}
|
||||
|
||||
// FileVisitor is wrapping around a StreamVisitor, to handle open/close files
|
||||
type FileVisitor struct {
|
||||
Path string
|
||||
*StreamVisitor
|
||||
}
|
||||
|
||||
// Visit in a FileVisitor is just taking care of opening/closing files
|
||||
func (v *FileVisitor) Visit(fn VisitorFunc) error {
|
||||
var f *os.File
|
||||
if v.Path == constSTDINstr {
|
||||
f = os.Stdin
|
||||
} else {
|
||||
var err error
|
||||
f, err = os.Open(v.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
// TODO: Consider adding a flag to force to UTF16, apparently some
|
||||
// Windows tools don't write the BOM
|
||||
utf16bom := unicode.BOMOverride(unicode.UTF8.NewDecoder())
|
||||
v.StreamVisitor.Reader = transform.NewReader(f, utf16bom)
|
||||
|
||||
return v.StreamVisitor.Visit(fn)
|
||||
}
|
||||
|
||||
// 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
|
||||
Schema ContentValidator
|
||||
}
|
||||
|
||||
// NewStreamVisitor is a helper function that is useful when we want to change the fields of the struct but keep calls the same.
|
||||
func NewStreamVisitor(r io.Reader, mapper *mapper, source string, schema ContentValidator) *StreamVisitor {
|
||||
return &StreamVisitor{
|
||||
Reader: r,
|
||||
mapper: mapper,
|
||||
Source: source,
|
||||
Schema: schema,
|
||||
}
|
||||
}
|
||||
|
||||
// Visit implements Visitor over a stream. StreamVisitor is able to distinct multiple resources in one stream.
|
||||
func (v *StreamVisitor) Visit(fn VisitorFunc) error {
|
||||
d := yaml.NewYAMLOrJSONDecoder(v.Reader, 4096)
|
||||
for {
|
||||
ext := runtime.RawExtension{}
|
||||
if err := d.Decode(&ext); err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("error parsing %s: %v", v.Source, err)
|
||||
}
|
||||
// TODO: This needs to be able to handle object in other encodings and schemas.
|
||||
ext.Raw = bytes.TrimSpace(ext.Raw)
|
||||
if len(ext.Raw) == 0 || bytes.Equal(ext.Raw, []byte("null")) {
|
||||
continue
|
||||
}
|
||||
if err := ValidateSchema(ext.Raw, v.Schema); err != nil {
|
||||
return fmt.Errorf("error validating %q: %v", v.Source, err)
|
||||
}
|
||||
info, err := v.infoForData(ext.Raw, v.Source)
|
||||
if err != nil {
|
||||
if fnErr := fn(info, err); fnErr != nil {
|
||||
return fnErr
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := fn(info, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateObjectNamespace(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Object != nil {
|
||||
return metadataAccessor.SetNamespace(info.Object, info.Namespace)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterNamespace omits the namespace if the object is not namespace scoped
|
||||
func FilterNamespace(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Namespaced() {
|
||||
info.Namespace = ""
|
||||
UpdateObjectNamespace(info, nil)
|
||||
}
|
||||
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, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Namespaced() {
|
||||
return nil
|
||||
}
|
||||
if len(info.Namespace) == 0 {
|
||||
info.Namespace = namespace
|
||||
UpdateObjectNamespace(info, nil)
|
||||
}
|
||||
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, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Namespaced() {
|
||||
return nil
|
||||
}
|
||||
if len(info.Namespace) == 0 {
|
||||
info.Namespace = namespace
|
||||
UpdateObjectNamespace(info, nil)
|
||||
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, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if meta.IsListType(info.Object) {
|
||||
return fmt.Errorf("watch is only supported on individual resources and resource collections, but a list of resources is found")
|
||||
}
|
||||
if len(info.Name) == 0 {
|
||||
return nil
|
||||
}
|
||||
if info.Namespaced() && len(info.Namespace) == 0 {
|
||||
return fmt.Errorf("no namespace set on resource %s %q", info.Mapping.Resource, info.Name)
|
||||
}
|
||||
return info.Get()
|
||||
}
|
||||
|
||||
// RetrieveLazy updates the object if it has not been loaded yet.
|
||||
func RetrieveLazy(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Object == nil {
|
||||
return info.Get()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FilterFunc func(info *Info, err error) (bool, error)
|
||||
|
||||
type FilteredVisitor struct {
|
||||
visitor Visitor
|
||||
filters []FilterFunc
|
||||
}
|
||||
|
||||
func NewFilteredVisitor(v Visitor, fn ...FilterFunc) Visitor {
|
||||
if len(fn) == 0 {
|
||||
return v
|
||||
}
|
||||
return FilteredVisitor{v, fn}
|
||||
}
|
||||
|
||||
func (v FilteredVisitor) Visit(fn VisitorFunc) error {
|
||||
return v.visitor.Visit(func(info *Info, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, filter := range v.filters {
|
||||
ok, err := filter(info, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fn(info, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func FilterByLabelSelector(s labels.Selector) FilterFunc {
|
||||
return func(info *Info, err error) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
a, err := meta.Accessor(info.Object)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !s.Matches(labels.Set(a.GetLabels())) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
type InfoListVisitor []*Info
|
||||
|
||||
func (infos InfoListVisitor) Visit(fn VisitorFunc) error {
|
||||
var err error
|
||||
for _, i := range infos {
|
||||
err = fn(i, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user