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  }