...

Source file src/k8s.io/cli-runtime/pkg/resource/helper.go

Documentation: k8s.io/cli-runtime/pkg/resource

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package resource
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  
    23  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    24  	"k8s.io/apimachinery/pkg/api/meta"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  
    27  	"k8s.io/apimachinery/pkg/fields"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/apimachinery/pkg/watch"
    31  )
    32  
    33  var metadataAccessor = meta.NewAccessor()
    34  
    35  // Helper provides methods for retrieving or mutating a RESTful
    36  // resource.
    37  type Helper struct {
    38  	// The name of this resource as the server would recognize it
    39  	Resource string
    40  	// The name of the subresource as the server would recognize it
    41  	Subresource string
    42  	// A RESTClient capable of mutating this resource.
    43  	RESTClient RESTClient
    44  	// True if the resource type is scoped to namespaces
    45  	NamespaceScoped bool
    46  	// If true, then use server-side dry-run to not persist changes to storage
    47  	// for verbs and resources that support server-side dry-run.
    48  	//
    49  	// Note this should only be used against an apiserver with dry-run enabled,
    50  	// and on resources that support dry-run. If the apiserver or the resource
    51  	// does not support dry-run, then the change will be persisted to storage.
    52  	ServerDryRun bool
    53  
    54  	// FieldManager is the name associated with the actor or entity that is making
    55  	// changes.
    56  	FieldManager string
    57  
    58  	// FieldValidation is the directive used to indicate how the server should perform
    59  	// field validation (Ignore, Warn, or Strict)
    60  	FieldValidation string
    61  }
    62  
    63  // NewHelper creates a Helper from a ResourceMapping
    64  func NewHelper(client RESTClient, mapping *meta.RESTMapping) *Helper {
    65  	return &Helper{
    66  		Resource:        mapping.Resource.Resource,
    67  		RESTClient:      client,
    68  		NamespaceScoped: mapping.Scope.Name() == meta.RESTScopeNameNamespace,
    69  	}
    70  }
    71  
    72  // DryRun, if true, will use server-side dry-run to not persist changes to storage.
    73  // Otherwise, changes will be persisted to storage.
    74  func (m *Helper) DryRun(dryRun bool) *Helper {
    75  	m.ServerDryRun = dryRun
    76  	return m
    77  }
    78  
    79  // WithFieldManager sets the field manager option to indicate the actor or entity
    80  // that is making changes in a create or update operation.
    81  func (m *Helper) WithFieldManager(fieldManager string) *Helper {
    82  	m.FieldManager = fieldManager
    83  	return m
    84  }
    85  
    86  // WithFieldValidation sets the field validation option to indicate
    87  // how the server should perform field validation (Ignore, Warn, or Strict).
    88  func (m *Helper) WithFieldValidation(validationDirective string) *Helper {
    89  	m.FieldValidation = validationDirective
    90  	return m
    91  }
    92  
    93  // Subresource sets the helper to access (<resource>/[ns/<namespace>/]<name>/<subresource>)
    94  func (m *Helper) WithSubresource(subresource string) *Helper {
    95  	m.Subresource = subresource
    96  	return m
    97  }
    98  
    99  func (m *Helper) Get(namespace, name string) (runtime.Object, error) {
   100  	req := m.RESTClient.Get().
   101  		NamespaceIfScoped(namespace, m.NamespaceScoped).
   102  		Resource(m.Resource).
   103  		Name(name).
   104  		SubResource(m.Subresource)
   105  	return req.Do(context.TODO()).Get()
   106  }
   107  
   108  func (m *Helper) List(namespace, apiVersion string, options *metav1.ListOptions) (runtime.Object, error) {
   109  	req := m.RESTClient.Get().
   110  		NamespaceIfScoped(namespace, m.NamespaceScoped).
   111  		Resource(m.Resource).
   112  		VersionedParams(options, metav1.ParameterCodec)
   113  	return req.Do(context.TODO()).Get()
   114  }
   115  
   116  // FollowContinue handles the continue parameter returned by the API server when using list
   117  // chunking. To take advantage of this, the initial ListOptions provided by the consumer
   118  // should include a non-zero Limit parameter.
   119  func FollowContinue(initialOpts *metav1.ListOptions,
   120  	listFunc func(metav1.ListOptions) (runtime.Object, error)) error {
   121  	opts := initialOpts
   122  	for {
   123  		list, err := listFunc(*opts)
   124  		if err != nil {
   125  			return err
   126  		}
   127  		nextContinueToken, _ := metadataAccessor.Continue(list)
   128  		if len(nextContinueToken) == 0 {
   129  			return nil
   130  		}
   131  		opts.Continue = nextContinueToken
   132  	}
   133  }
   134  
   135  // EnhanceListError augments errors typically returned by List operations with additional context,
   136  // making sure to retain the StatusError type when applicable.
   137  func EnhanceListError(err error, opts metav1.ListOptions, subj string) error {
   138  	if apierrors.IsResourceExpired(err) {
   139  		return err
   140  	}
   141  	if apierrors.IsBadRequest(err) || apierrors.IsNotFound(err) {
   142  		if se, ok := err.(*apierrors.StatusError); ok {
   143  			// modify the message without hiding this is an API error
   144  			if len(opts.LabelSelector) == 0 && len(opts.FieldSelector) == 0 {
   145  				se.ErrStatus.Message = fmt.Sprintf("Unable to list %q: %v", subj,
   146  					se.ErrStatus.Message)
   147  			} else {
   148  				se.ErrStatus.Message = fmt.Sprintf(
   149  					"Unable to find %q that match label selector %q, field selector %q: %v", subj,
   150  					opts.LabelSelector,
   151  					opts.FieldSelector, se.ErrStatus.Message)
   152  			}
   153  			return se
   154  		}
   155  		if len(opts.LabelSelector) == 0 && len(opts.FieldSelector) == 0 {
   156  			return fmt.Errorf("Unable to list %q: %v", subj, err)
   157  		}
   158  		return fmt.Errorf("Unable to find %q that match label selector %q, field selector %q: %v",
   159  			subj, opts.LabelSelector, opts.FieldSelector, err)
   160  	}
   161  	return err
   162  }
   163  
   164  func (m *Helper) Watch(namespace, apiVersion string, options *metav1.ListOptions) (watch.Interface, error) {
   165  	options.Watch = true
   166  	return m.RESTClient.Get().
   167  		NamespaceIfScoped(namespace, m.NamespaceScoped).
   168  		Resource(m.Resource).
   169  		VersionedParams(options, metav1.ParameterCodec).
   170  		Watch(context.TODO())
   171  }
   172  
   173  func (m *Helper) WatchSingle(namespace, name, resourceVersion string) (watch.Interface, error) {
   174  	return m.RESTClient.Get().
   175  		NamespaceIfScoped(namespace, m.NamespaceScoped).
   176  		Resource(m.Resource).
   177  		VersionedParams(&metav1.ListOptions{
   178  			ResourceVersion: resourceVersion,
   179  			Watch:           true,
   180  			FieldSelector:   fields.OneTermEqualSelector("metadata.name", name).String(),
   181  		}, metav1.ParameterCodec).
   182  		Watch(context.TODO())
   183  }
   184  
   185  func (m *Helper) Delete(namespace, name string) (runtime.Object, error) {
   186  	return m.DeleteWithOptions(namespace, name, nil)
   187  }
   188  
   189  func (m *Helper) DeleteWithOptions(namespace, name string, options *metav1.DeleteOptions) (runtime.Object, error) {
   190  	if options == nil {
   191  		options = &metav1.DeleteOptions{}
   192  	}
   193  	if m.ServerDryRun {
   194  		options.DryRun = []string{metav1.DryRunAll}
   195  	}
   196  
   197  	return m.RESTClient.Delete().
   198  		NamespaceIfScoped(namespace, m.NamespaceScoped).
   199  		Resource(m.Resource).
   200  		Name(name).
   201  		Body(options).
   202  		Do(context.TODO()).
   203  		Get()
   204  }
   205  
   206  func (m *Helper) Create(namespace string, modify bool, obj runtime.Object) (runtime.Object, error) {
   207  	return m.CreateWithOptions(namespace, modify, obj, nil)
   208  }
   209  
   210  func (m *Helper) CreateWithOptions(namespace string, modify bool, obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
   211  	if options == nil {
   212  		options = &metav1.CreateOptions{}
   213  	}
   214  	if m.ServerDryRun {
   215  		options.DryRun = []string{metav1.DryRunAll}
   216  	}
   217  	if m.FieldManager != "" {
   218  		options.FieldManager = m.FieldManager
   219  	}
   220  	if m.FieldValidation != "" {
   221  		options.FieldValidation = m.FieldValidation
   222  	}
   223  	if modify {
   224  		// Attempt to version the object based on client logic.
   225  		version, err := metadataAccessor.ResourceVersion(obj)
   226  		if err != nil {
   227  			// We don't know how to clear the version on this object, so send it to the server as is
   228  			return m.createResource(m.RESTClient, m.Resource, namespace, obj, options)
   229  		}
   230  		if version != "" {
   231  			if err := metadataAccessor.SetResourceVersion(obj, ""); err != nil {
   232  				return nil, err
   233  			}
   234  		}
   235  	}
   236  
   237  	return m.createResource(m.RESTClient, m.Resource, namespace, obj, options)
   238  }
   239  
   240  func (m *Helper) createResource(c RESTClient, resource, namespace string, obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
   241  	return c.Post().
   242  		NamespaceIfScoped(namespace, m.NamespaceScoped).
   243  		Resource(resource).
   244  		VersionedParams(options, metav1.ParameterCodec).
   245  		Body(obj).
   246  		Do(context.TODO()).
   247  		Get()
   248  }
   249  func (m *Helper) Patch(namespace, name string, pt types.PatchType, data []byte, options *metav1.PatchOptions) (runtime.Object, error) {
   250  	if options == nil {
   251  		options = &metav1.PatchOptions{}
   252  	}
   253  	if m.ServerDryRun {
   254  		options.DryRun = []string{metav1.DryRunAll}
   255  	}
   256  	if m.FieldManager != "" {
   257  		options.FieldManager = m.FieldManager
   258  	}
   259  	if m.FieldValidation != "" {
   260  		options.FieldValidation = m.FieldValidation
   261  	}
   262  	return m.RESTClient.Patch(pt).
   263  		NamespaceIfScoped(namespace, m.NamespaceScoped).
   264  		Resource(m.Resource).
   265  		Name(name).
   266  		SubResource(m.Subresource).
   267  		VersionedParams(options, metav1.ParameterCodec).
   268  		Body(data).
   269  		Do(context.TODO()).
   270  		Get()
   271  }
   272  
   273  func (m *Helper) Replace(namespace, name string, overwrite bool, obj runtime.Object) (runtime.Object, error) {
   274  	c := m.RESTClient
   275  	var options = &metav1.UpdateOptions{}
   276  	if m.ServerDryRun {
   277  		options.DryRun = []string{metav1.DryRunAll}
   278  	}
   279  	if m.FieldManager != "" {
   280  		options.FieldManager = m.FieldManager
   281  	}
   282  	if m.FieldValidation != "" {
   283  		options.FieldValidation = m.FieldValidation
   284  	}
   285  
   286  	// Attempt to version the object based on client logic.
   287  	version, err := metadataAccessor.ResourceVersion(obj)
   288  	if err != nil {
   289  		// We don't know how to version this object, so send it to the server as is
   290  		return m.replaceResource(c, m.Resource, namespace, name, obj, options)
   291  	}
   292  	if version == "" && overwrite {
   293  		// Retrieve the current version of the object to overwrite the server object
   294  		serverObj, err := c.Get().NamespaceIfScoped(namespace, m.NamespaceScoped).Resource(m.Resource).Name(name).SubResource(m.Subresource).Do(context.TODO()).Get()
   295  		if err != nil {
   296  			// The object does not exist, but we want it to be created
   297  			return m.replaceResource(c, m.Resource, namespace, name, obj, options)
   298  		}
   299  		serverVersion, err := metadataAccessor.ResourceVersion(serverObj)
   300  		if err != nil {
   301  			return nil, err
   302  		}
   303  		if err := metadataAccessor.SetResourceVersion(obj, serverVersion); err != nil {
   304  			return nil, err
   305  		}
   306  	}
   307  
   308  	return m.replaceResource(c, m.Resource, namespace, name, obj, options)
   309  }
   310  
   311  func (m *Helper) replaceResource(c RESTClient, resource, namespace, name string, obj runtime.Object, options *metav1.UpdateOptions) (runtime.Object, error) {
   312  	return c.Put().
   313  		NamespaceIfScoped(namespace, m.NamespaceScoped).
   314  		Resource(resource).
   315  		Name(name).
   316  		SubResource(m.Subresource).
   317  		VersionedParams(options, metav1.ParameterCodec).
   318  		Body(obj).
   319  		Do(context.TODO()).
   320  		Get()
   321  }
   322  

View as plain text