github.com/verrazzano/verrazzano@v1.7.0/pkg/k8s/resource/resource_util.go (about) 1 // Copyright (c) 2020, 2023, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package resource 5 6 import ( 7 "bufio" 8 "bytes" 9 "context" 10 "fmt" 11 "io" 12 "os" 13 "strings" 14 15 "go.uber.org/zap" 16 17 "github.com/verrazzano/verrazzano/pkg/k8sutil" 18 "github.com/verrazzano/verrazzano/pkg/log/vzlog" 19 "k8s.io/apimachinery/pkg/api/errors" 20 meta "k8s.io/apimachinery/pkg/api/meta" 21 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 "k8s.io/apimachinery/pkg/runtime/schema" 24 "k8s.io/apimachinery/pkg/types" 25 utilyaml "k8s.io/apimachinery/pkg/util/yaml" 26 "k8s.io/client-go/discovery" 27 memory "k8s.io/client-go/discovery/cached" 28 "k8s.io/client-go/dynamic" 29 "k8s.io/client-go/rest" 30 "k8s.io/client-go/restmapper" 31 "sigs.k8s.io/yaml" 32 ) 33 34 var nsGvr = schema.GroupVersionResource{ 35 Group: "", 36 Version: "v1", 37 Resource: "namespaces", 38 } 39 40 // CreateOrUpdateResourceFromFile creates or updates a Kubernetes resources from a YAML file. 41 // This is intended to be equivalent to `kubectl apply` 42 // The cluster used is the one set by default in the environment 43 func CreateOrUpdateResourceFromFile(file string, log *zap.SugaredLogger) error { 44 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 45 if err != nil { 46 log.Errorf("Error getting kubeconfig, error: %v", err) 47 return err 48 } 49 50 return CreateOrUpdateResourceFromFileInCluster(file, kubeconfigPath) 51 } 52 53 // CreateOrUpdateResourceFromBytes creates or updates a Kubernetes resources from a YAML data byte array. 54 // The cluster used is the one set by default in the environment 55 func CreateOrUpdateResourceFromBytes(data []byte, log *zap.SugaredLogger) error { 56 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 57 if err != nil { 58 log.Errorf("Error getting kubeconfig, error: %v", err) 59 return err 60 } 61 62 config, err := k8sutil.GetKubeConfigGivenPath(kubeconfigPath) 63 if err != nil { 64 return fmt.Errorf("failed to get kube config: %w", err) 65 } 66 67 return CreateOrUpdateResourceFromBytesUsingConfig(data, config) 68 } 69 70 // CreateOrUpdateResourceFromFileInCluster is identical to CreateOrUpdateResourceFromFile, except that 71 // it uses the cluster specified by the kubeconfigPath argument instead of the default cluster in the environment 72 func CreateOrUpdateResourceFromFileInCluster(file string, kubeconfigPath string) error { 73 bytes, err := os.ReadFile(file) 74 if err != nil { 75 return fmt.Errorf("failed to read test data file: %w", err) 76 } 77 78 config, err := k8sutil.GetKubeConfigGivenPath(kubeconfigPath) 79 if err != nil { 80 return fmt.Errorf("failed to get kube config: %w", err) 81 } 82 return CreateOrUpdateResourceFromBytesUsingConfig(bytes, config) 83 } 84 85 // CreateOrUpdateResourceFromBytesUsingConfig creates or updates a Kubernetes resource from bytes. 86 // This is intended to be equivalent to `kubectl apply` 87 func CreateOrUpdateResourceFromBytesUsingConfig(data []byte, config *rest.Config) error { 88 client, err := dynamic.NewForConfig(config) 89 if err != nil { 90 return fmt.Errorf("failed to create dynamic client: %w", err) 91 } 92 disco, err := discovery.NewDiscoveryClientForConfig(config) 93 if err != nil { 94 return fmt.Errorf("failed to create discovery client: %w", err) 95 } 96 mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(disco)) 97 98 reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data))) 99 for { 100 // Unmarshall the YAML bytes into an Unstructured. 101 uns := &unstructured.Unstructured{ 102 Object: map[string]interface{}{}, 103 } 104 unsMap, err := readNextResourceFromBytes(reader, mapper, client, uns, "") 105 if err != nil { 106 return fmt.Errorf("failed to read resource from bytes: %w", err) 107 } 108 if unsMap == nil { 109 // all resources must have been read 110 return nil 111 } 112 113 namespace := uns.GetNamespace() 114 var cli dynamic.ResourceInterface 115 116 if namespace != "" { 117 cli = client.Resource(unsMap.Resource).Namespace(namespace) 118 } else { 119 cli = client.Resource(unsMap.Resource) 120 } 121 122 // Attempt to create the resource. 123 _, err = cli.Create(context.TODO(), uns, metav1.CreateOptions{}) 124 if err != nil && errors.IsAlreadyExists(err) { 125 // Get, read the resource version, and then update the resource. 126 resource, err := cli.Get(context.TODO(), uns.GetName(), metav1.GetOptions{}) 127 if err != nil { 128 return fmt.Errorf("failed to get resource for update: %w", err) 129 } 130 uns.SetResourceVersion(resource.GetResourceVersion()) 131 _, err = cli.Update(context.TODO(), uns, metav1.UpdateOptions{}) 132 if err != nil { 133 return fmt.Errorf("failed to update resource: %w", err) 134 } 135 } else if err != nil { 136 return fmt.Errorf("failed to create resource: %w", err) 137 } 138 } 139 // no return since you can't get here 140 } 141 142 // CreateOrUpdateResourceFromFileInGeneratedNamespace creates or updates a Kubernetes resources from a YAML file. 143 // Namespaces are not in the resource yaml files. They are generated and passed in 144 // Resources will be created in the namespace that is passed in 145 // This is intended to be equivalent to `kubectl apply` 146 // The cluster used is the one set by default in the environment 147 func CreateOrUpdateResourceFromFileInGeneratedNamespace(file string, namespace string) error { 148 var logger = vzlog.DefaultLogger() 149 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 150 if err != nil { 151 logger.Errorf("Error getting kubeconfig, error: %v", err) 152 return err 153 } 154 155 return CreateOrUpdateResourceFromFileInClusterInGeneratedNamespace(file, kubeconfigPath, namespace) 156 } 157 158 // CreateOrUpdateResourceFromFileInClusterInGeneratedNamespace is identical to CreateOrUpdateResourceFromFileInGeneratedNamespace, except that 159 // it uses the cluster specified by the kubeconfigPath argument instead of the default cluster in the environment 160 func CreateOrUpdateResourceFromFileInClusterInGeneratedNamespace(file string, kubeconfigPath string, namespace string) error { 161 bytes, err := os.ReadFile(file) 162 if err != nil { 163 return fmt.Errorf("failed to read test data file: %w", err) 164 } 165 166 config, err := k8sutil.GetKubeConfigGivenPath(kubeconfigPath) 167 if err != nil { 168 return fmt.Errorf("failed to get kube config: %w", err) 169 } 170 171 return createOrUpdateResourceFromBytesInNamespace(bytes, config, namespace) 172 } 173 174 // createOrUpdateResourceFromBytesInNamespace creates or updates a Kubernetes resource from bytes in the provided namespace. 175 // This is intended to be equivalent to `kubectl apply` 176 func createOrUpdateResourceFromBytesInNamespace(data []byte, config *rest.Config, namespace string) error { 177 client, err := dynamic.NewForConfig(config) 178 if err != nil { 179 return fmt.Errorf("failed to create dynamic client: %w", err) 180 } 181 disco, err := discovery.NewDiscoveryClientForConfig(config) 182 if err != nil { 183 return fmt.Errorf("failed to create discovery client: %w", err) 184 } 185 mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(disco)) 186 187 reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data))) 188 for { 189 // Unmarshall the YAML bytes into an Unstructured. 190 uns := &unstructured.Unstructured{ 191 Object: map[string]interface{}{}, 192 } 193 unsMap, err := readNextResourceFromBytes(reader, mapper, client, uns, namespace) 194 if err != nil { 195 return fmt.Errorf("failed to read resource from bytes: %w", err) 196 } 197 if unsMap == nil { 198 // all resources must have been read 199 return nil 200 } 201 uns.SetNamespace(namespace) 202 203 // Attempt to create the resource. 204 _, err = client.Resource(unsMap.Resource).Namespace(namespace).Create(context.TODO(), uns, metav1.CreateOptions{}) 205 if err != nil && errors.IsAlreadyExists(err) { 206 // Get, read the resource version, and then update the resource. 207 resource, err := client.Resource(unsMap.Resource).Namespace(namespace).Get(context.TODO(), uns.GetName(), metav1.GetOptions{}) 208 if err != nil { 209 return fmt.Errorf("failed to get resource for update: %w", err) 210 } 211 uns.SetResourceVersion(resource.GetResourceVersion()) 212 _, err = client.Resource(unsMap.Resource).Namespace(namespace).Update(context.TODO(), uns, metav1.UpdateOptions{}) 213 if err != nil { 214 return fmt.Errorf("failed to update resource: %w", err) 215 } 216 } else if err != nil { 217 return fmt.Errorf("failed to create resource: %w", err) 218 } 219 } 220 // no return since you can't get here 221 } 222 223 func readNextResourceFromBytes(reader *utilyaml.YAMLReader, mapper *restmapper.DeferredDiscoveryRESTMapper, client dynamic.Interface, uns *unstructured.Unstructured, namespace string) (*meta.RESTMapping, error) { 224 // Read one section of the YAML 225 buf, err := reader.Read() 226 227 // Return success if the whole file has been read. 228 if err == io.EOF { 229 return nil, nil 230 } else if err != nil { 231 return nil, fmt.Errorf("failed to read resource section: %w", err) 232 } 233 234 if err = yaml.Unmarshal(buf, &uns.Object); err != nil { 235 return nil, fmt.Errorf("failed to unmarshal resource: %w", err) 236 } 237 238 // If namespace is nil, then try to get it from uns 239 if namespace == "" { 240 namespace = uns.GetNamespace() 241 } 242 243 // If the resource has a namespace, check to make sure the namespace exists. 244 if namespace != "" { 245 _, err = client.Resource(nsGvr).Get(context.TODO(), namespace, metav1.GetOptions{}) 246 if err != nil { 247 return nil, fmt.Errorf("failed to find resource namespace: %w", err) 248 } 249 } 250 251 // Map the object's GVK to a GVR 252 unsGvk := schema.FromAPIVersionAndKind(uns.GetAPIVersion(), uns.GetKind()) 253 unsMap, err := mapper.RESTMapping(unsGvk.GroupKind(), unsGvk.Version) 254 if err != nil { 255 return unsMap, fmt.Errorf("failed to map resource kind: %w", err) 256 } 257 return unsMap, nil 258 } 259 260 // DeleteResourceFromFile deletes Kubernetes resources using names found in a YAML file. 261 // This is intended to be equivalent to `kubectl delete` 262 func DeleteResourceFromFile(file string, log *zap.SugaredLogger) error { 263 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 264 if err != nil { 265 log.Errorf("Error getting kubeconfig, error: %v", err) 266 return err 267 } 268 return DeleteResourceFromFileInCluster(file, kubeconfigPath) 269 } 270 271 // DeleteResourceFromFileInCluster is identical to DeleteResourceFromFile, except that 272 // // it uses the cluster specified by the kubeconfigPath argument instead of the default cluster in the environment 273 func DeleteResourceFromFileInCluster(file string, kubeconfigPath string) error { 274 bytes, err := os.ReadFile(file) 275 if err != nil { 276 return fmt.Errorf("failed to read test data file: %w", err) 277 } 278 config, err := k8sutil.GetKubeConfigGivenPath(kubeconfigPath) 279 if err != nil { 280 return fmt.Errorf("failed to get kube config: %w", err) 281 } 282 283 return deleteResourceFromBytes(bytes, config) 284 } 285 286 // deleteResourceFromBytes deletes Kubernetes resources using names found in YAML bytes. 287 // This is intended to be equivalent to `kubectl delete` 288 func deleteResourceFromBytes(data []byte, config *rest.Config) error { 289 client, err := dynamic.NewForConfig(config) 290 if err != nil { 291 return fmt.Errorf("failed to create dynamic client: %w", err) 292 } 293 disco, err := discovery.NewDiscoveryClientForConfig(config) 294 if err != nil { 295 return fmt.Errorf("failed to create discovery client: %w", err) 296 } 297 mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(disco)) 298 299 reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data))) 300 for { 301 // Unmarshall the YAML bytes into an Unstructured. 302 uns := &unstructured.Unstructured{ 303 Object: map[string]interface{}{}, 304 } 305 unsMap, err := readNextResourceFromBytes(reader, mapper, client, uns, "") 306 if err != nil { 307 return fmt.Errorf("failed to read resource from bytes: %w", err) 308 } 309 if unsMap == nil { 310 // all resources must have been read 311 return nil 312 } 313 314 // Delete the resource. 315 err = client.Resource(unsMap.Resource).Namespace(uns.GetNamespace()).Delete(context.TODO(), uns.GetName(), metav1.DeleteOptions{}) 316 if err != nil && !errors.IsNotFound(err) { 317 fmt.Printf("Failed to delete %s/%v", uns.GetNamespace(), uns.GroupVersionKind()) 318 } 319 } 320 } 321 322 // DeleteResourceFromFileInGeneratedNamespace deletes Kubernetes resources using names found in a YAML file. 323 // The namespace is generated and passed in 324 func DeleteResourceFromFileInGeneratedNamespace(file string, namespace string) error { 325 var logger = vzlog.DefaultLogger() 326 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 327 if err != nil { 328 logger.Errorf("Error getting kubeconfig, error: %v", err) 329 return err 330 } 331 return DeleteResourceFromFileInClusterInGeneratedNamespace(file, kubeconfigPath, namespace) 332 } 333 334 // DeleteResourceFromFileInClusterInGeneratedNamespace is identical to DeleteResourceFromFileInGeneratedNamespace, 335 // except that it uses the cluster specified by the kubeconfigPath argument instead of the default cluster in the environment 336 func DeleteResourceFromFileInClusterInGeneratedNamespace(file string, kubeconfigPath string, namespace string) error { 337 bytes, err := os.ReadFile(file) 338 if err != nil { 339 return fmt.Errorf("failed to read test data file: %w", err) 340 } 341 config, err := k8sutil.GetKubeConfigGivenPath(kubeconfigPath) 342 if err != nil { 343 return fmt.Errorf("failed to get kube config: %w", err) 344 } 345 346 return deleteResourceFromBytesInNamespace(bytes, config, namespace) 347 } 348 349 // deleteResourceFromBytesInNamespace deletes Kubernetes resources using names found in YAML bytes. 350 // This is intended to be equivalent to `kubectl delete` 351 func deleteResourceFromBytesInNamespace(data []byte, config *rest.Config, namespace string) error { 352 client, err := dynamic.NewForConfig(config) 353 if err != nil { 354 return fmt.Errorf("failed to create dynamic client: %w", err) 355 } 356 disco, err := discovery.NewDiscoveryClientForConfig(config) 357 if err != nil { 358 return fmt.Errorf("failed to create discovery client: %w", err) 359 } 360 mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(disco)) 361 362 reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data))) 363 for { 364 // Unmarshall the YAML bytes into an Unstructured. 365 uns := &unstructured.Unstructured{ 366 Object: map[string]interface{}{}, 367 } 368 unsMap, err := readNextResourceFromBytes(reader, mapper, client, uns, namespace) 369 if err != nil { 370 return fmt.Errorf("failed to read resource from bytes: %w", err) 371 } 372 if unsMap == nil { 373 // all resources must have been read 374 return nil 375 } 376 377 // Delete the resource. 378 err = client.Resource(unsMap.Resource).Namespace(namespace).Delete(context.TODO(), uns.GetName(), metav1.DeleteOptions{}) 379 if err != nil && !errors.IsNotFound(err) { 380 fmt.Printf("Failed to delete %s/%v", namespace, uns.GroupVersionKind()) 381 } 382 } 383 } 384 385 // PatchResourceFromFileInCluster patches a Kubernetes resource from a given patch file in the specified cluster 386 // If the given patch file has a ".yaml" extension, the contents will be converted to JSON 387 // This is intended to be equivalent to `kubectl patch` 388 func PatchResourceFromFileInCluster(gvr schema.GroupVersionResource, namespace string, name string, patchFile string, kubeconfigPath string) error { 389 patchBytes, err := os.ReadFile(patchFile) 390 if err != nil { 391 return fmt.Errorf("failed to read test data file: %w", err) 392 } 393 394 if strings.HasSuffix(patchFile, ".yaml") { 395 patchBytes, err = utilyaml.ToJSON(patchBytes) 396 if err != nil { 397 return fmt.Errorf("could not convert patch data to JSON: %w", err) 398 } 399 } 400 401 config, err := k8sutil.GetKubeConfigGivenPath(kubeconfigPath) 402 if err != nil { 403 return fmt.Errorf("failed to get kube config: %w", err) 404 } 405 406 return PatchResourceFromBytes(gvr, types.MergePatchType, namespace, name, patchBytes, config) 407 } 408 409 // PatchResourceFromBytes patches a Kubernetes resource from bytes. The contents of the byte slice must be in 410 // JSON format. This is intended to be equivalent to `kubectl patch`. 411 func PatchResourceFromBytes(gvr schema.GroupVersionResource, patchType types.PatchType, namespace string, name string, patchDataJSON []byte, config *rest.Config) error { 412 client, err := dynamic.NewForConfig(config) 413 if err != nil { 414 return fmt.Errorf("failed to create dynamic client: %w", err) 415 } 416 417 // Attempt to patch the resource. 418 _, err = client.Resource(gvr).Namespace(namespace).Patch(context.TODO(), name, patchType, patchDataJSON, metav1.PatchOptions{}) 419 if err != nil { 420 return fmt.Errorf("failed to patch %s/%v: %w", namespace, gvr, err) 421 } 422 return nil 423 }