github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/lib/operatorclient/customresources.go (about)

     1  package operatorclient
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"path"
     8  	"strings"
     9  	"time"
    10  
    11  	"k8s.io/apimachinery/pkg/api/errors"
    12  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    15  	"k8s.io/apimachinery/pkg/util/wait"
    16  	"k8s.io/klog"
    17  )
    18  
    19  // CustomResourceList represents a list of custom resource objects that will
    20  // be returned from a List() operation.
    21  type CustomResourceList struct {
    22  	metav1.TypeMeta   `json:",inline"`
    23  	metav1.ObjectMeta `json:"metadata"`
    24  
    25  	Items []*unstructured.Unstructured `json:"items"`
    26  }
    27  
    28  // GetCustomResource returns the custom resource as *unstructured.Unstructured by the given name.
    29  func (c *Client) GetCustomResource(apiGroup, version, namespace, resourcePlural, resourceName string) (*unstructured.Unstructured, error) {
    30  	klog.V(4).Infof("[GET CUSTOM RESOURCE]: %s:%s", namespace, resourceName)
    31  	var object unstructured.Unstructured
    32  
    33  	b, err := c.GetCustomResourceRaw(apiGroup, version, namespace, resourcePlural, resourceName)
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  
    38  	if err := json.Unmarshal(b, &object); err != nil {
    39  		return nil, fmt.Errorf("failed to unmarshal CUSTOM RESOURCE: %v", err)
    40  	}
    41  	return &object, nil
    42  }
    43  
    44  // GetCustomResourceRaw returns the custom resource's raw body data by the given name.
    45  func (c *Client) GetCustomResourceRaw(apiGroup, version, namespace, resourcePlural, resourceName string) ([]byte, error) {
    46  	klog.V(4).Infof("[GET CUSTOM RESOURCE RAW]: %s:%s", namespace, resourceName)
    47  	httpRestClient := c.extInterface.ApiextensionsV1beta1().RESTClient()
    48  	uri := customResourceURI(apiGroup, version, namespace, resourcePlural, resourceName)
    49  	klog.V(4).Infof("[GET]: %s", uri)
    50  
    51  	return httpRestClient.Get().RequestURI(uri).DoRaw(context.TODO())
    52  }
    53  
    54  // CreateCustomResource creates the custom resource.
    55  func (c *Client) CreateCustomResource(item *unstructured.Unstructured) error {
    56  	klog.V(4).Infof("[CREATE CUSTOM RESOURCE]: %s:%s", item.GetNamespace(), item.GetName())
    57  	kind := item.GetKind()
    58  	namespace := item.GetNamespace()
    59  	apiVersion := item.GetAPIVersion()
    60  	apiGroup, version, err := parseAPIVersion(apiVersion)
    61  	if err != nil {
    62  		return err
    63  	}
    64  
    65  	data, err := json.Marshal(item)
    66  	if err != nil {
    67  		return err
    68  	}
    69  
    70  	return c.CreateCustomResourceRaw(apiGroup, version, namespace, kind, data)
    71  }
    72  
    73  // CreateCustomResourceRaw creates the raw bytes of the custom resource.
    74  func (c *Client) CreateCustomResourceRaw(apiGroup, version, namespace, kind string, data []byte) error {
    75  	klog.V(4).Infof("[CREATE CUSTOM RESOURCE RAW]: %s:%s", namespace, kind)
    76  	var statusCode int
    77  
    78  	httpRestClient := c.extInterface.ApiextensionsV1beta1().RESTClient()
    79  	uri := customResourceDefinitionURI(apiGroup, version, namespace, kind)
    80  	klog.V(4).Infof("[POST]: %s", uri)
    81  	result := httpRestClient.Post().RequestURI(uri).Body(data).Do(context.TODO())
    82  
    83  	if result.Error() != nil {
    84  		return result.Error()
    85  	}
    86  
    87  	result.StatusCode(&statusCode)
    88  	klog.V(4).Infof("Written %s, status: %d", uri, statusCode)
    89  
    90  	if statusCode != 201 {
    91  		return fmt.Errorf("unexpected status code %d, expecting 201", statusCode)
    92  	}
    93  	return nil
    94  }
    95  
    96  // CreateCustomResourceRawIfNotFound creates the raw bytes of the custom resource if it doesn't exist, or Updates if it does exist.
    97  // It also returns a boolean to indicate whether a new custom resource is created.
    98  func (c *Client) CreateCustomResourceRawIfNotFound(apiGroup, version, namespace, kind, name string, data []byte) (bool, error) {
    99  	klog.V(4).Infof("[CREATE CUSTOM RESOURCE RAW if not found]: %s:%s", namespace, name)
   100  	_, err := c.GetCustomResource(apiGroup, version, namespace, kind, name)
   101  	if err == nil {
   102  		return false, nil
   103  	}
   104  	if !errors.IsNotFound(err) {
   105  		return false, err
   106  	}
   107  	err = c.CreateCustomResourceRaw(apiGroup, version, namespace, kind, data)
   108  	if apierrors.IsAlreadyExists(err) {
   109  		if err = c.UpdateCustomResourceRaw(apiGroup, version, namespace, kind, name, data); err != nil {
   110  			return false, err
   111  		}
   112  	} else if err != nil {
   113  		return false, err
   114  	}
   115  	return true, nil
   116  }
   117  
   118  // UpdateCustomResource updates the custom resource.
   119  // To do an atomic update, use AtomicModifyCustomResource().
   120  func (c *Client) UpdateCustomResource(item *unstructured.Unstructured) error {
   121  	klog.V(4).Infof("[UPDATE CUSTOM RESOURCE]: %s:%s", item.GetNamespace(), item.GetName())
   122  	kind := item.GetKind()
   123  	name := item.GetName()
   124  	namespace := item.GetNamespace()
   125  	apiVersion := item.GetAPIVersion()
   126  	apiGroup, version, err := parseAPIVersion(apiVersion)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	data, err := json.Marshal(item)
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	return c.UpdateCustomResourceRaw(apiGroup, version, namespace, kind, name, data)
   137  }
   138  
   139  // UpdateCustomResourceRaw updates the thirdparty resource with the raw data.
   140  func (c *Client) UpdateCustomResourceRaw(apiGroup, version, namespace, resourcePlural, resourceName string, data []byte) error {
   141  	klog.V(4).Infof("[UPDATE CUSTOM RESOURCE RAW]: %s:%s", namespace, resourceName)
   142  	var statusCode int
   143  
   144  	httpRestClient := c.extInterface.ApiextensionsV1beta1().RESTClient()
   145  	uri := customResourceURI(apiGroup, version, namespace, resourcePlural, resourceName)
   146  	klog.V(4).Infof("[PUT]: %s", uri)
   147  	result := httpRestClient.Put().RequestURI(uri).Body(data).Do(context.TODO())
   148  
   149  	if result.Error() != nil {
   150  		return result.Error()
   151  	}
   152  
   153  	result.StatusCode(&statusCode)
   154  	klog.V(4).Infof("Updated %s, status: %d", uri, statusCode)
   155  
   156  	if statusCode != 200 {
   157  		return fmt.Errorf("unexpected status code %d, expecting 200", statusCode)
   158  	}
   159  	return nil
   160  }
   161  
   162  // CreateOrUpdateCustomeResourceRaw creates the custom resource if it doesn't exist.
   163  // If the custom resource exists, it updates the existing one.
   164  func (c *Client) CreateOrUpdateCustomeResourceRaw(apiGroup, version, namespace, resourcePlural, resourceName string, data []byte) error {
   165  	klog.V(4).Infof("[CREATE OR UPDATE UPDATE CUSTOM RESOURCE RAW]: %s:%s", namespace, resourceName)
   166  	old, err := c.GetCustomResourceRaw(apiGroup, version, namespace, resourcePlural, resourceName)
   167  	if err != nil {
   168  		if !errors.IsNotFound(err) {
   169  			return err
   170  		}
   171  		return c.CreateCustomResourceRaw(apiGroup, version, namespace, resourcePlural, data)
   172  	}
   173  
   174  	var oldSpec, newSpec unstructured.Unstructured
   175  	if err := json.Unmarshal(old, &oldSpec); err != nil {
   176  		return err
   177  	}
   178  	if err := json.Unmarshal(data, &newSpec); err != nil {
   179  		return err
   180  	}
   181  
   182  	// Set the resource version.
   183  	newSpec.SetResourceVersion(oldSpec.GetResourceVersion())
   184  
   185  	data, err = json.Marshal(&newSpec)
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	return c.UpdateCustomResourceRaw(apiGroup, version, namespace, resourcePlural, resourceName, data)
   191  }
   192  
   193  // DeleteCustomResource deletes the  with the given name.
   194  func (c *Client) DeleteCustomResource(apiGroup, version, namespace, resourcePlural, resourceName string) error {
   195  	klog.V(4).Infof("[DELETE CUSTOM RESOURCE]: %s:%s", namespace, resourceName)
   196  	httpRestClient := c.extInterface.ApiextensionsV1beta1().RESTClient()
   197  	uri := customResourceURI(apiGroup, version, namespace, resourcePlural, resourceName)
   198  
   199  	klog.V(4).Infof("[DELETE]: %s", uri)
   200  	_, err := httpRestClient.Delete().RequestURI(uri).DoRaw(context.TODO())
   201  	return err
   202  }
   203  
   204  // CustomResourceModifier takes the custom resource object, and modifies it in-place.
   205  type CustomResourceModifier func(*unstructured.Unstructured, interface{}) error
   206  
   207  // AtomicModifyCustomResource gets the custom resource, modifies it and writes it back.
   208  // If it's modified by other writers, we will retry until it succeeds.
   209  func (c *Client) AtomicModifyCustomResource(apiGroup, version, namespace, resourcePlural, resourceName string, f CustomResourceModifier, data interface{}) error {
   210  	klog.V(4).Infof("[ATOMIC MODIFY CUSTOM RESOURCE]: %s:%s", namespace, resourceName)
   211  	return wait.PollInfinite(time.Second, func() (bool, error) {
   212  		var customResource unstructured.Unstructured
   213  		b, err := c.GetCustomResourceRaw(apiGroup, version, namespace, resourcePlural, resourceName)
   214  		if err != nil {
   215  			klog.Errorf("Failed to get CUSTOM RESOURCE %q, kind:%q: %v", resourceName, resourcePlural, err)
   216  			return false, err
   217  		}
   218  
   219  		if err := json.Unmarshal(b, &customResource); err != nil {
   220  			klog.Errorf("Failed to unmarshal CUSTOM RESOURCE %q, kind:%q: %v", resourceName, resourcePlural, err)
   221  			return false, err
   222  		}
   223  
   224  		if err := f(&customResource, data); err != nil {
   225  			klog.Errorf("Failed to modify the CUSTOM RESOURCE %q, kind:%q: %v", resourceName, resourcePlural, err)
   226  			return false, err
   227  		}
   228  
   229  		if err := c.UpdateCustomResource(&customResource); err != nil {
   230  			if errors.IsConflict(err) {
   231  				klog.Errorf("Failed to update CUSTOM RESOURCE %q, kind:%q: %v, will retry", resourceName, resourcePlural, err)
   232  				return false, nil
   233  			}
   234  			klog.Errorf("Failed to update CUSTOM RESOURCE %q, kind:%q: %v", resourceName, resourcePlural, err)
   235  			return false, err
   236  		}
   237  
   238  		return true, nil
   239  	})
   240  }
   241  
   242  // customResourceURI returns the URI for the thirdparty resource.
   243  //
   244  // Example of apiGroup: "tco.coreos.com"
   245  // Example of version: "v1"
   246  // Example of namespace: "default"
   247  // Example of resourcePlural: "ChannelOperatorConfigs"
   248  // Example of resourceName: "test-config"
   249  func customResourceURI(apiGroup, version, namespace, resourcePlural, resourceName string) string {
   250  	if namespace == "" {
   251  		namespace = metav1.NamespaceDefault
   252  	}
   253  
   254  	return fmt.Sprintf("/apis/%s/%s/namespaces/%s/%s/%s",
   255  		strings.ToLower(apiGroup),
   256  		strings.ToLower(version),
   257  		strings.ToLower(namespace),
   258  		strings.ToLower(resourcePlural),
   259  		strings.ToLower(resourceName))
   260  }
   261  
   262  // customResourceDefinitionURI returns the URI for the CRD.
   263  //
   264  // Example of apiGroup: "tco.coreos.com"
   265  // Example of version: "v1"
   266  // Example of namespace: "default"
   267  // Example of resourcePlural: "ChannelOperatorConfigs"
   268  func customResourceDefinitionURI(apiGroup, version, namespace, resourcePlural string) string {
   269  	if namespace == "" {
   270  		namespace = metav1.NamespaceDefault
   271  	}
   272  
   273  	return fmt.Sprintf("/apis/%s/%s/namespaces/%s/%s",
   274  		strings.ToLower(apiGroup),
   275  		strings.ToLower(version),
   276  		strings.ToLower(namespace),
   277  		strings.ToLower(resourcePlural))
   278  }
   279  
   280  // ListCustomResource lists all custom resources for the given namespace.
   281  func (c *Client) ListCustomResource(apiGroup, version, namespace, resourcePlural string) (*CustomResourceList, error) {
   282  	klog.V(4).Infof("LIST CUSTOM RESOURCE]: %s", resourcePlural)
   283  
   284  	var crList CustomResourceList
   285  
   286  	httpRestClient := c.extInterface.ApiextensionsV1beta1().RESTClient()
   287  	uri := customResourceDefinitionURI(apiGroup, version, namespace, resourcePlural)
   288  	klog.V(4).Infof("[GET]: %s", uri)
   289  	bytes, err := httpRestClient.Get().RequestURI(uri).DoRaw(context.TODO())
   290  	if err != nil {
   291  		return nil, fmt.Errorf("failed to get custom resource list: %v", err)
   292  	}
   293  
   294  	if err := json.Unmarshal(bytes, &crList); err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	return &crList, nil
   299  }
   300  
   301  // parseAPIVersion splits "coreos.com/v1" into
   302  // "coreos.com" and "v1".
   303  func parseAPIVersion(apiVersion string) (apiGroup, version string, err error) {
   304  	parts := strings.Split(apiVersion, "/")
   305  	if len(parts) < 2 {
   306  		return "", "", fmt.Errorf("invalid format of api version %q, expecting APIGroup/Version", apiVersion)
   307  	}
   308  	return path.Join(parts[:len(parts)-1]...), parts[len(parts)-1], nil
   309  }