github.com/splunk/dan1-qbec@v0.7.3/internal/remote/client.go (about)

     1  /*
     2     Copyright 2019 Splunk Inc.
     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 remote
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/ghodss/yaml"
    26  	"github.com/jonboulle/clockwork"
    27  	"github.com/pkg/errors"
    28  	"github.com/splunk/qbec/internal/model"
    29  	"github.com/splunk/qbec/internal/remote/k8smeta"
    30  	"github.com/splunk/qbec/internal/sio"
    31  	"github.com/splunk/qbec/internal/types"
    32  	apiErrors "k8s.io/apimachinery/pkg/api/errors"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	apiTypes "k8s.io/apimachinery/pkg/types"
    37  	"k8s.io/client-go/discovery"
    38  	"k8s.io/client-go/dynamic"
    39  )
    40  
    41  const (
    42  	identicalObjects = "objects are identical"
    43  	opUpdate         = "update object"
    44  	opCreate         = "create object"
    45  )
    46  
    47  // structured errors
    48  var (
    49  	ErrForbidden        = errors.New("forbidden")             // returned due to an authn/ authz error
    50  	ErrNotFound         = errors.New("not found")             // returned when a remote object does not exist
    51  	errMetadataNotFound = errors.New("server type not found") // returned when metadata could not be found for a gvk
    52  )
    53  
    54  // this file contains the client definition and supported CRUD operations.
    55  
    56  // SyncOptions provides the caller with options for the sync operation.
    57  type SyncOptions struct {
    58  	DryRun        bool // do not actually create or update objects, return what would happen
    59  	DisableCreate bool // only update objects if they exist, do not create new ones
    60  	ShowSecrets   bool // show secrets in patches and creations
    61  }
    62  
    63  type internalSyncOptions struct {
    64  	secretDryRun       bool               // dry-run phase for objects having secrets info
    65  	pristiner          pristineReadWriter // pristine writer
    66  	pristineAnnotation string             // pristine annotation to manipulate for secrets dry-run
    67  }
    68  
    69  // Client is a thick remote client that provides high-level operations for commands as opposed to
    70  // granular ones.
    71  type Client struct {
    72  	resources    *k8smeta.Resources               // the server metadata loaded once and never updated
    73  	schema       *k8smeta.ServerSchema            // the server schema
    74  	pool         dynamic.ClientPool               // the client pool for resource interfaces
    75  	disco        k8smeta.ResourceDiscovery        // the discovery interface
    76  	defaultNs    string                           // the default namespace to set for namespaced objects that do not define one
    77  	verbosity    int                              // log verbosity
    78  	dynamicTypes map[schema.GroupVersionKind]bool // crds seen by this client
    79  }
    80  
    81  func newClient(pool dynamic.ClientPool, disco discovery.DiscoveryInterface, ns string, verbosity int) (*Client, error) {
    82  	start := time.Now()
    83  	resources, err := k8smeta.NewResources(disco, k8smeta.ResourceOpts{WarnFn: sio.Warnln})
    84  	if err != nil {
    85  		return nil, errors.Wrap(err, "get server metadata")
    86  	}
    87  	if verbosity > 0 {
    88  		resources.Dump(sio.Debugln)
    89  	}
    90  	duration := time.Since(start).Round(time.Millisecond)
    91  	sio.Debugln("cluster metadata load took", duration)
    92  
    93  	ss := k8smeta.NewServerSchema(disco)
    94  	c := &Client{
    95  		resources:    resources,
    96  		schema:       ss,
    97  		pool:         pool,
    98  		disco:        disco,
    99  		defaultNs:    ns,
   100  		verbosity:    verbosity,
   101  		dynamicTypes: map[schema.GroupVersionKind]bool{},
   102  	}
   103  	return c, nil
   104  }
   105  
   106  // ValidatorFor returns a validator for the supplied group version kind.
   107  func (c *Client) ValidatorFor(gvk schema.GroupVersionKind) (k8smeta.Validator, error) {
   108  	return c.schema.ValidatorFor(gvk)
   109  }
   110  
   111  // objectNamespace returns the namespace for the specified object. It returns a blank
   112  // string when the object is cluster-scoped. For namespace-scoped objects it returns
   113  // the default namespace when the object does not have one set. It does not fail if the
   114  // object type is not known and just returns whatever is specified for the object.
   115  func (c *Client) objectNamespace(o model.K8sMeta) string {
   116  	info := c.resources.APIResource(o.GroupVersionKind())
   117  	ns := o.GetNamespace()
   118  	if info != nil {
   119  		if info.Namespaced {
   120  			if ns == "" {
   121  				ns = c.defaultNs
   122  			}
   123  		} else {
   124  			ns = ""
   125  		}
   126  	}
   127  	return ns
   128  }
   129  
   130  // DisplayName returns the display name of the supplied K8s object.
   131  func (c *Client) DisplayName(o model.K8sMeta) string {
   132  	sm := c.resources
   133  	gvk := o.GroupVersionKind()
   134  	info := sm.APIResource(gvk)
   135  
   136  	displayType := func() string {
   137  		if info != nil {
   138  			return info.Name
   139  		}
   140  		return strings.ToLower(gvk.Kind)
   141  	}
   142  
   143  	displayName := func() string {
   144  		ns := c.objectNamespace(o)
   145  		name := model.NameForDisplay(o)
   146  		if ns == "" {
   147  			return name
   148  		}
   149  		return name + " -n " + ns
   150  	}
   151  	name := fmt.Sprintf("%s %s", displayType(), displayName())
   152  	if l, ok := o.(model.K8sLocalObject); ok {
   153  		comp := l.Component()
   154  		if comp != "" {
   155  			name += fmt.Sprintf(" (source %s)", comp)
   156  		}
   157  	}
   158  	return name
   159  }
   160  
   161  func (c *Client) apiResourceFor(gvk schema.GroupVersionKind) (*metav1.APIResource, error) {
   162  	info := c.resources.APIResource(gvk)
   163  	if info == nil {
   164  		return nil, fmt.Errorf("resource not found for %s/%s %s", gvk.Group, gvk.Version, gvk.Kind)
   165  	}
   166  	return info, nil
   167  }
   168  
   169  // IsNamespaced returns if the supplied group version kind is namespaced.
   170  func (c *Client) IsNamespaced(gvk schema.GroupVersionKind) (bool, error) {
   171  	res, err := c.apiResourceFor(gvk)
   172  	if err != nil {
   173  		return false, err
   174  	}
   175  	return res.Namespaced, nil
   176  }
   177  
   178  func (c *Client) canonicalGroupVersionKind(in schema.GroupVersionKind) (schema.GroupVersionKind, error) {
   179  	return c.resources.CanonicalGroupVersionKind(in)
   180  }
   181  
   182  // Get returns the remote object matching the supplied metadata as an unstructured bag of attributes.
   183  func (c *Client) Get(obj model.K8sMeta) (*unstructured.Unstructured, error) {
   184  	rc, err := c.resourceInterfaceWithDefaultNs(obj.GroupVersionKind(), obj.GetNamespace())
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	u, err := rc.Get(obj.GetName(), metav1.GetOptions{})
   189  	if err != nil {
   190  		if apiErrors.IsNotFound(err) {
   191  			return nil, ErrNotFound
   192  		}
   193  		if apiErrors.IsForbidden(err) {
   194  			return nil, ErrForbidden
   195  		}
   196  		return nil, err
   197  	}
   198  	return u, nil
   199  }
   200  
   201  // ObjectKey returns a string key for the supplied object that includes its group-kind,
   202  // namespace and name. Input values are used in case canonical values cannot be derived
   203  // (e.g. for custom resources whose CRDs haven't yet been created).
   204  func (c *Client) ObjectKey(obj model.K8sMeta) string {
   205  	gvk := obj.GroupVersionKind()
   206  	if canon, err := c.resources.CanonicalGroupVersionKind(gvk); err == nil {
   207  		gvk = canon
   208  	}
   209  	ns := c.objectNamespace(obj)
   210  	return fmt.Sprintf("%s:%s:%s:%s", gvk.Group, gvk.Kind, ns, obj.GetName())
   211  }
   212  
   213  // ListQueryScope defines the scope at which list queries need to be executed.
   214  type ListQueryScope struct {
   215  	Namespaces     []string // namespaces of interest
   216  	ClusterObjects bool     // whether to query for cluster objects
   217  }
   218  
   219  // ListQueryConfig is the config with which to execute list queries.
   220  type ListQueryConfig struct {
   221  	Application         string       // must be non-blank
   222  	Tag                 string       // may be blank
   223  	Environment         string       // must be non-blank
   224  	ListQueryScope                   // the query scope for namespaces and non-namespaced resources
   225  	KindFilter          model.Filter // filters for object kind
   226  	Concurrency         int          // concurrent queries to execute
   227  	DisableAllNsQueries bool         // do not perform list queries across namespaces when multiple namespaces in picture
   228  }
   229  
   230  // Collection represents a set of k8s objects with the ability to remove a subset of objects from it.
   231  type Collection interface {
   232  	Remove(obj []model.K8sQbecMeta) error // remove all objects represented by the input list
   233  	ToList() []model.K8sQbecMeta          // return a list of remaining objects
   234  }
   235  
   236  // ListObjects returns all objects for the application and environment for the namespace /cluster scopes
   237  // and kind filtering indicated by the query configuration.
   238  func (c *Client) ListObjects(scope ListQueryConfig) (Collection, error) {
   239  	if scope.KindFilter == nil {
   240  		kf, _ := model.NewKindFilter(nil, nil)
   241  		scope.KindFilter = kf
   242  	}
   243  
   244  	// handle special cases
   245  	filterEligibleTypes := func(types []schema.GroupVersionKind) []schema.GroupVersionKind {
   246  		var ret []schema.GroupVersionKind
   247  		for _, t := range types {
   248  			switch {
   249  			// the issue with endpoints is that every service creates endpoints objects and
   250  			// propagates its own labels to it. These have not been created by qbec.
   251  			case t.Group == "" && t.Kind == "Endpoints":
   252  				if c.verbosity > 0 {
   253  					sio.Debugf("not listing objects of type %v\n", t)
   254  				}
   255  			default:
   256  				ret = append(ret, t)
   257  			}
   258  		}
   259  		return ret
   260  	}
   261  
   262  	var namespacedTypes, clusterTypes []schema.GroupVersionKind
   263  	for _, v := range c.resources.CanonicalResources() {
   264  		gvk := schema.GroupVersionKind{Group: v.Group, Version: v.Version, Kind: v.Kind}
   265  		if v.Namespaced {
   266  			namespacedTypes = append(namespacedTypes, gvk)
   267  		} else {
   268  			clusterTypes = append(clusterTypes, gvk)
   269  		}
   270  	}
   271  
   272  	qc := queryConfig{
   273  		scope:            scope,
   274  		resourceProvider: c.ResourceInterface,
   275  		namespacedTypes:  filterEligibleTypes(namespacedTypes),
   276  		clusterTypes:     filterEligibleTypes(clusterTypes),
   277  		verbosity:        c.verbosity,
   278  	}
   279  	ol := objectLister{qc}
   280  	coll := newCollection(c.defaultNs, c)
   281  	if err := ol.serverObjects(coll); err != nil {
   282  		return nil, err
   283  	}
   284  	return coll, nil
   285  }
   286  
   287  type updateResult struct {
   288  	SkipReason    string             `json:"skip,omitempty"`
   289  	Operation     string             `json:"operation,omitempty"`
   290  	Source        string             `json:"source,omitempty"`
   291  	Kind          apiTypes.PatchType `json:"kind,omitempty"`
   292  	DisplayPatch  string             `json:"patch,omitempty"`
   293  	GeneratedName string             `json:"generatedName,omitempty"`
   294  	patch         []byte
   295  }
   296  
   297  func (u *updateResult) String() string {
   298  	b, err := yaml.Marshal(u)
   299  	if err != nil {
   300  		sio.Warnln("unable to marshal result to YAML")
   301  	}
   302  	return string(b)
   303  }
   304  
   305  func (u *updateResult) toSyncResult() *SyncResult {
   306  	switch {
   307  	case u.SkipReason == identicalObjects:
   308  		return &SyncResult{
   309  			Type:    SyncObjectsIdentical,
   310  			Details: u.SkipReason,
   311  		}
   312  	case u.SkipReason != "":
   313  		return &SyncResult{
   314  			Type:    SyncSkip,
   315  			Details: u.SkipReason,
   316  		}
   317  	case u.Operation == opCreate:
   318  		return &SyncResult{
   319  			Type:          SyncCreated,
   320  			GeneratedName: u.GeneratedName, // only set when name actually generated
   321  			Details:       u.String(),
   322  		}
   323  	case u.Operation == opUpdate:
   324  		return &SyncResult{
   325  			Type:    SyncUpdated,
   326  			Details: u.String(),
   327  		}
   328  	default:
   329  		panic(fmt.Errorf("invalid operation:%s, %v", u.Operation, u))
   330  	}
   331  }
   332  
   333  // SyncResultType indicates what notionally happened in a sync operation.
   334  type SyncResultType int
   335  
   336  // Sync result types
   337  const (
   338  	_                    SyncResultType = iota
   339  	SyncObjectsIdentical                // sync was a noop due to local and remote being identical
   340  	SyncSkip                            // object was skipped for sync (e.g. creation needed but disabled)
   341  	SyncCreated                         // object was created
   342  	SyncUpdated                         // object was updated
   343  	SyncDeleted                         // object was deleted
   344  )
   345  
   346  // SyncResult is the result of a sync operation. There is no difference in the output for a real versus
   347  // a dry-run.
   348  type SyncResult struct {
   349  	Type          SyncResultType // the result type
   350  	GeneratedName string         // the actual name of an object that has generateName set
   351  	Details       string         // additional details that are safe to print to console (e.g. no secrets)
   352  }
   353  
   354  func extractCustomTypes(obj model.K8sObject) (schema.GroupVersionKind, error) {
   355  	var ret schema.GroupVersionKind
   356  	var crd struct {
   357  		Spec struct {
   358  			Group   string `json:"group"`
   359  			Version string `json:"version"`
   360  			Names   struct {
   361  				Kind string `json:"kind"`
   362  			} `json:"names"`
   363  		} `json:"spec"`
   364  	}
   365  	b, err := obj.ToUnstructured().MarshalJSON()
   366  	if err != nil {
   367  		return ret, err
   368  	}
   369  	if err := json.Unmarshal(b, &crd); err != nil {
   370  		return ret, err
   371  	}
   372  	return schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Spec.Names.Kind}, nil
   373  }
   374  
   375  // Sync syncs the local object by either creating a new one or patching an existing one.
   376  // It does not do anything in dry-run mode. It also does not create new objects if the caller has disabled the feature.
   377  func (c *Client) Sync(original model.K8sLocalObject, opts SyncOptions) (_ *SyncResult, finalError error) {
   378  	// set up the pristine strategy.
   379  	var prw pristineReadWriter = qbecPristine{}
   380  	sensitive := types.HasSensitiveInfo(original.ToUnstructured())
   381  
   382  	internal := internalSyncOptions{
   383  		secretDryRun:       false,
   384  		pristiner:          prw,
   385  		pristineAnnotation: model.QbecNames.PristineAnnotation,
   386  	}
   387  
   388  	if sensitive && !opts.ShowSecrets {
   389  		internal.secretDryRun = true
   390  	}
   391  
   392  	defer func() {
   393  		if finalError != nil {
   394  			finalError = errors.Wrap(finalError, "sync "+c.DisplayName(original))
   395  		}
   396  	}()
   397  
   398  	gvk := original.GroupVersionKind()
   399  	if gvk.Kind == "CustomResourceDefinition" && gvk.Group == "apiextensions.k8s.io" {
   400  		t, err := extractCustomTypes(original)
   401  		if err != nil {
   402  			sio.Warnf("error extracting types for custom resource %s, %v\n", original.GetName(), err)
   403  		} else {
   404  			c.dynamicTypes[t] = true
   405  		}
   406  	}
   407  
   408  	result, err := c.doSync(original, opts, internal)
   409  	if err != nil {
   410  		return nil, err
   411  	}
   412  	// exit if we are done
   413  	if !internal.secretDryRun || opts.DryRun {
   414  		return result.toSyncResult(), nil
   415  	}
   416  	internal.secretDryRun = false
   417  	_, err = c.doSync(original, opts, internal) // do the real sync
   418  	if err != nil {
   419  		return nil, err
   420  	}
   421  	return result.toSyncResult(), err
   422  }
   423  
   424  func (c *Client) doSync(original model.K8sLocalObject, opts SyncOptions, internal internalSyncOptions) (*updateResult, error) {
   425  	gvk := original.GroupVersionKind()
   426  	var remObj *unstructured.Unstructured
   427  	var objErr error
   428  	if original.GetName() != "" {
   429  		remObj, objErr = c.Get(original)
   430  	}
   431  	switch {
   432  	// empty name, always create
   433  	case original.GetName() == "":
   434  		break
   435  	// ignore object not found errors
   436  	case objErr == ErrNotFound:
   437  		break
   438  	// treat metadata errors (server type not found) as a "not found" error under the following conditions:
   439  	// - dry-run mode is active
   440  	// - a prior custom resource with that GVK has been applied
   441  	case objErr == errMetadataNotFound && opts.DryRun && c.dynamicTypes[gvk]:
   442  		break
   443  	// error but with better message
   444  	case objErr == errMetadataNotFound && opts.DryRun:
   445  		return nil, fmt.Errorf("server type %v not found and no prior CRD installs it", gvk)
   446  	// report all other errors
   447  	case objErr != nil:
   448  		return nil, objErr
   449  	}
   450  
   451  	var obj model.K8sLocalObject
   452  	if internal.secretDryRun {
   453  		opts.DryRun = true // won't affect caller since passed by value
   454  		obj, _ = types.HideSensitiveLocalInfo(original)
   455  	} else {
   456  		o, err := internal.pristiner.createFromPristine(original)
   457  		if err != nil {
   458  			return nil, errors.Wrap(err, "create from pristine")
   459  		}
   460  		obj = o
   461  	}
   462  
   463  	// create or update as needed, each of these routines is responsible for correct dry-run handling.
   464  	var result *updateResult
   465  	var err error
   466  	if remObj == nil {
   467  		result, err = c.maybeCreate(obj, opts)
   468  	} else {
   469  		if internal.secretDryRun {
   470  			ann := remObj.GetAnnotations()
   471  			if ann == nil {
   472  				ann = map[string]string{}
   473  			}
   474  			delete(ann, internal.pristineAnnotation)
   475  			remObj.SetAnnotations(ann)
   476  			c, _ := types.HideSensitiveInfo(remObj)
   477  			remObj = c
   478  		}
   479  		result, err = c.maybeUpdate(obj, remObj, opts)
   480  	}
   481  	if err != nil {
   482  		return nil, err
   483  	}
   484  
   485  	// create a prettier patch for display, if needed
   486  	result.DisplayPatch = string(result.patch)
   487  	if result.patch != nil {
   488  		var data interface{}
   489  		if err := json.Unmarshal(result.patch, &data); err == nil {
   490  			b, err := json.MarshalIndent(data, "", "    ")
   491  			if err == nil {
   492  				result.DisplayPatch = string(b)
   493  			}
   494  		}
   495  	}
   496  	return result, nil
   497  }
   498  
   499  // Delete delete the supplied object if it exists. It does not do anything in dry-run mode.
   500  func (c *Client) Delete(obj model.K8sMeta, dryRun bool) (_ *SyncResult, finalError error) {
   501  	ret := &SyncResult{
   502  		Type: SyncDeleted,
   503  	}
   504  	if dryRun {
   505  		return ret, nil
   506  	}
   507  	defer func() {
   508  		if finalError != nil {
   509  			finalError = errors.Wrap(finalError, "delete "+c.DisplayName(obj))
   510  		}
   511  	}()
   512  
   513  	ri, err := c.resourceInterfaceWithDefaultNs(obj.GroupVersionKind(), obj.GetNamespace())
   514  	if err != nil {
   515  		return nil, errors.Wrap(err, "get resource interface")
   516  	}
   517  
   518  	pp := metav1.DeletePropagationForeground
   519  	err = ri.Delete(obj.GetName(), &metav1.DeleteOptions{PropagationPolicy: &pp})
   520  	if err != nil {
   521  		if apiErrors.IsNotFound(err) {
   522  			ret.Type = SyncSkip
   523  			ret.Details = "object not found on the server"
   524  			return ret, nil
   525  		}
   526  		if apiErrors.IsConflict(err) && obj.GetKind() == "Namespace" {
   527  			ret.Type = SyncSkip
   528  			ret.Details = "namespace delete had conflict, ignore"
   529  			return ret, nil
   530  		}
   531  		return nil, err
   532  	}
   533  	return ret, nil
   534  }
   535  
   536  func (c *Client) jitResource(gvk schema.GroupVersionKind) (*metav1.APIResource, error) {
   537  	rl, err := c.disco.ServerResourcesForGroupVersion(gvk.GroupVersion().String())
   538  	if err != nil {
   539  		return nil, err
   540  	}
   541  	for _, r := range rl.APIResources {
   542  		if strings.Contains(r.Name, "/") { // ignore sub-resources
   543  			continue
   544  		}
   545  		if r.Kind == gvk.Kind {
   546  			return &r, nil
   547  		}
   548  	}
   549  	return nil, fmt.Errorf("server does not recognize gvk %s", gvk)
   550  }
   551  
   552  // ResourceInterface returns a dynamic resource interface for the supplied group version kind and namespace.
   553  func (c *Client) ResourceInterface(gvk schema.GroupVersionKind, namespace string) (dynamic.ResourceInterface, error) {
   554  	client, err := c.pool.ClientForGroupVersionKind(gvk)
   555  	if err != nil {
   556  		return nil, err
   557  	}
   558  	res, err := c.apiResourceFor(gvk)
   559  	if err != nil { // could be a resource for a CRD that was just created, re-query discovery
   560  		res, err = c.jitResource(gvk)
   561  		if err != nil {
   562  			return nil, errMetadataNotFound
   563  		}
   564  	}
   565  	return client.Resource(res, namespace), nil
   566  }
   567  
   568  func (c *Client) resourceInterfaceWithDefaultNs(gvk schema.GroupVersionKind, namespace string) (dynamic.ResourceInterface, error) {
   569  	if namespace == "" {
   570  		namespace = c.defaultNs
   571  	}
   572  	return c.ResourceInterface(gvk, namespace)
   573  }
   574  
   575  func (c *Client) maybeCreate(obj model.K8sLocalObject, opts SyncOptions) (*updateResult, error) {
   576  	if opts.DisableCreate {
   577  		return &updateResult{
   578  			SkipReason: "creation disabled due to user request",
   579  		}, nil
   580  	}
   581  	b, err := json.Marshal(obj)
   582  	if err != nil {
   583  		return nil, errors.Wrap(err, "json marshal")
   584  	}
   585  	result := &updateResult{
   586  		Operation: opCreate,
   587  		Source:    "local",
   588  		patch:     b,
   589  	}
   590  	if opts.DryRun {
   591  		return result, nil
   592  	}
   593  	ri, err := c.resourceInterfaceWithDefaultNs(obj.GroupVersionKind(), obj.GetNamespace())
   594  	if err != nil {
   595  		return nil, errors.Wrap(err, "get resource interface")
   596  	}
   597  	out, err := ri.Create(obj.ToUnstructured())
   598  	if err != nil {
   599  		return nil, err
   600  	}
   601  	if obj.GetName() == "" {
   602  		result.GeneratedName = out.GetName()
   603  	}
   604  	return result, nil
   605  }
   606  
   607  func (c *Client) maybeUpdate(obj model.K8sLocalObject, remObj *unstructured.Unstructured, opts SyncOptions) (*updateResult, error) {
   608  	res, err := c.schema.OpenAPIResources()
   609  	if err != nil {
   610  		sio.Warnln("get open API resources", err)
   611  	}
   612  	var lookup openAPILookup
   613  	if res != nil {
   614  		lookup = res.LookupResource
   615  	}
   616  
   617  	p := patcher{
   618  		provider: c.resourceInterfaceWithDefaultNs,
   619  		cfgProvider: func(obj *unstructured.Unstructured) ([]byte, error) {
   620  			pristine, _ := getPristineVersion(obj, false)
   621  			if pristine == nil {
   622  				p := map[string]interface{}{
   623  					"kind":       obj.GetKind(),
   624  					"apiVersion": obj.GetAPIVersion(),
   625  					"metadata": map[string]interface{}{
   626  						"name": obj.GetName(),
   627  					},
   628  				}
   629  				pb, _ := json.Marshal(p)
   630  				return pb, nil
   631  			}
   632  			b, _ := json.Marshal(pristine)
   633  			return b, nil
   634  		},
   635  		overwrite:     true,
   636  		backOff:       clockwork.NewRealClock(),
   637  		openAPILookup: lookup,
   638  	}
   639  
   640  	var result *updateResult
   641  	if opts.DryRun {
   642  		result, err = p.getPatchContents(remObj, obj)
   643  	} else {
   644  		result, err = p.patch(remObj, obj)
   645  	}
   646  	return result, err
   647  }