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 }