github.com/verrazzano/verrazzano@v1.7.1/pkg/k8sutil/apply_yaml.go (about)

     1  // Copyright (c) 2021, 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 k8sutil
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"os"
    14  	"path"
    15  	"reflect"
    16  	"strings"
    17  	"text/template"
    18  
    19  	"github.com/verrazzano/verrazzano/pkg/kubectlutil"
    20  	"k8s.io/apimachinery/pkg/api/errors"
    21  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    22  	controllerruntime "sigs.k8s.io/controller-runtime"
    23  	crtpkg "sigs.k8s.io/controller-runtime/pkg/client"
    24  	"sigs.k8s.io/yaml"
    25  )
    26  
    27  const (
    28  	sep = "---"
    29  )
    30  
    31  type (
    32  	YAMLApplier struct {
    33  		client            crtpkg.Client
    34  		objects           []unstructured.Unstructured
    35  		namespaceOverride string
    36  		objectResultMsgs  []string
    37  	}
    38  
    39  	action func(obj *unstructured.Unstructured) error
    40  )
    41  
    42  // funcMap contains the helper functions used during templating
    43  var funcMap template.FuncMap = map[string]any{
    44  	"contains":        strings.Contains,
    45  	"nindent":         nindent,
    46  	"multiLineIndent": multiLineIndent,
    47  }
    48  
    49  func NewYAMLApplier(client crtpkg.Client, namespaceOverride string) *YAMLApplier {
    50  	return &YAMLApplier{
    51  		client:            client,
    52  		objects:           []unstructured.Unstructured{},
    53  		namespaceOverride: namespaceOverride,
    54  		objectResultMsgs:  []string{},
    55  	}
    56  }
    57  
    58  // Objects is the list of objects created using the ApplyX methods
    59  func (y *YAMLApplier) Objects() []unstructured.Unstructured {
    60  	return y.objects
    61  }
    62  
    63  // ObjectResultMsgs is the list of object result messages using the ApplyX methods
    64  func (y *YAMLApplier) ObjectResultMsgs() []string {
    65  	return y.objectResultMsgs
    66  }
    67  
    68  // ApplyD applies all YAML files in a directory to Kubernetes
    69  func (y *YAMLApplier) ApplyD(directory string) error {
    70  	files, err := os.ReadDir(directory)
    71  	if err != nil {
    72  		return err
    73  	}
    74  	filteredFiles := filterYamlExt(files)
    75  	if len(filteredFiles) < 1 {
    76  		return fmt.Errorf("no files passed to apply: %s", directory)
    77  	}
    78  	for _, file := range filteredFiles {
    79  		filePath := path.Join(directory, file.Name())
    80  		if err = y.ApplyF(filePath); err != nil {
    81  			return err
    82  		}
    83  	}
    84  
    85  	return nil
    86  }
    87  
    88  // ApplyDT applies a directory of file templates to Kubernetes
    89  func (y *YAMLApplier) ApplyDT(directory string, args any) error {
    90  	files, err := os.ReadDir(directory)
    91  	if err != nil {
    92  		return err
    93  	}
    94  	filteredFiles := filterYamlExt(files)
    95  	if len(filteredFiles) < 1 {
    96  		return fmt.Errorf("no files passed to apply: %s", directory)
    97  	}
    98  	for _, file := range filteredFiles {
    99  		filePath := path.Join(directory, file.Name())
   100  		if err = y.ApplyFT(filePath, args); err != nil {
   101  			return err
   102  		}
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  func (y *YAMLApplier) ApplyBT(b []byte, args any) error {
   109  	return y.doTemplatedBytesAction(b, y.applyAction, args)
   110  }
   111  
   112  // ApplyF applies a file spec to Kubernetes
   113  func (y *YAMLApplier) ApplyF(filePath string) error {
   114  	return y.doFileAction(filePath, y.applyAction)
   115  }
   116  
   117  // ApplyS applies a spec to Kubernetes via a string
   118  func (y *YAMLApplier) ApplyS(spec string) error {
   119  	return y.doStringAction(spec, y.applyAction)
   120  }
   121  
   122  // ApplyFT applies a file template spec (go text.template) to Kubernetes
   123  func (y *YAMLApplier) ApplyFT(filePath string, args any) error {
   124  	return y.doTemplatedFileAction(filePath, y.applyAction, args)
   125  }
   126  
   127  // ApplyFTDefaultConfig calls ApplyFT with rest client from the default config
   128  func (y *YAMLApplier) ApplyFTDefaultConfig(filePath string, args any) error {
   129  	config, err := GetKubeConfig()
   130  	if err != nil {
   131  		return err
   132  	}
   133  	client, err := crtpkg.New(config, crtpkg.Options{})
   134  	if err != nil {
   135  		return err
   136  	}
   137  	y.client = client
   138  	return y.ApplyFT(filePath, args)
   139  }
   140  
   141  // DeleteF deletes a file spec from Kubernetes
   142  func (y *YAMLApplier) DeleteF(filePath string) error {
   143  	return y.doFileAction(filePath, y.deleteAction)
   144  }
   145  
   146  // DeleteS deletes resources in a spec from Kubernetes via a string
   147  func (y *YAMLApplier) DeleteS(spec string) error {
   148  	return y.doStringAction(spec, y.deleteAction)
   149  }
   150  
   151  // DeleteFWithDependents deletes a file spec from Kubernetes along with other dependent objects in the background
   152  func (y *YAMLApplier) DeleteFWithDependents(filePath string) error {
   153  	return y.doFileAction(filePath, y.deleteActionWithDependents)
   154  }
   155  
   156  // DeleteFT deletes a file template spec (go text.template) to Kubernetes
   157  func (y *YAMLApplier) DeleteFT(filePath string, args any) error {
   158  	return y.doTemplatedFileAction(filePath, y.deleteAction, args)
   159  }
   160  
   161  // DeleteFTDefaultConfig calls deleteFT with rest client from the default config
   162  func (y *YAMLApplier) DeleteFTDefaultConfig(filePath string, args any) error {
   163  	config, err := GetKubeConfig()
   164  	if err != nil {
   165  		return err
   166  	}
   167  	client, err := crtpkg.New(config, crtpkg.Options{})
   168  	if err != nil {
   169  		return err
   170  	}
   171  	y.client = client
   172  	return y.DeleteFT(filePath, args)
   173  }
   174  
   175  // applyAction creates a merge patch of the object with the server object
   176  func (y *YAMLApplier) applyAction(obj *unstructured.Unstructured) error {
   177  	var ns = strings.TrimSpace(y.namespaceOverride)
   178  	if len(ns) > 0 {
   179  		obj.SetNamespace(ns)
   180  	}
   181  
   182  	// Struct to store a copy of a client field
   183  	type clientField struct {
   184  		name       string
   185  		nestedCopy interface{}
   186  		typeOf     string
   187  	}
   188  
   189  	// Make a nested copy of each client field.
   190  	var clientFields []clientField
   191  	var err error
   192  	for fieldName, fieldObj := range obj.Object {
   193  		if fieldName == "kind" || fieldName == "apiVersion" {
   194  			continue
   195  		}
   196  		cf := clientField{}
   197  		cf.name = fieldName
   198  		cf.nestedCopy, _, err = unstructured.NestedFieldCopy(obj.Object, fieldName)
   199  		if err != nil {
   200  			return err
   201  		}
   202  		cf.typeOf = reflect.TypeOf(fieldObj).String()
   203  		clientFields = append(clientFields, cf)
   204  	}
   205  
   206  	err = kubectlutil.SetLastAppliedConfigurationAnnotation(obj)
   207  	if err != nil {
   208  		return err
   209  	}
   210  
   211  	result, err := controllerruntime.CreateOrUpdate(context.TODO(), y.client, obj, func() error {
   212  		// For each nested copy of a client field, determine if it needs to be added or merged
   213  		// with the server.
   214  		for _, clientField := range clientFields {
   215  
   216  			serverField, _, err := unstructured.NestedFieldCopy(obj.Object, clientField.name)
   217  			if err != nil {
   218  				return err
   219  			}
   220  
   221  			// See if merge needed on objects of type map[string]interface {}
   222  			if clientField.typeOf == "map[string]interface {}" {
   223  				if serverField != nil {
   224  					merge(serverField.(map[string]interface{}), clientField.nestedCopy.(map[string]interface{}))
   225  				}
   226  			}
   227  
   228  			// For objects of type []interface{}, e.g. secrets or imagePullSecrets, a replace will be
   229  			// done.  This appears to be consistent with the behavior of kubectl.
   230  			if clientField.typeOf == "[]interface {}" {
   231  				serverField = clientField.nestedCopy
   232  			}
   233  
   234  			// If serverSpec is nil, then the clientSpec field is being added
   235  			if serverField == nil {
   236  				serverField = clientField.nestedCopy
   237  			}
   238  
   239  			// Set the resulting value in the server object
   240  			err = unstructured.SetNestedField(obj.Object, serverField, clientField.name)
   241  			if err != nil {
   242  				return err
   243  			}
   244  		}
   245  		// Delete any keys in server obj not included in the client fields.
   246  		for key := range obj.Object {
   247  			if key == "kind" || key == "apiVersion" {
   248  				continue
   249  			}
   250  			keyFound := false
   251  			for _, clientField := range clientFields {
   252  				if clientField.name == key {
   253  					keyFound = true
   254  					break
   255  				}
   256  			}
   257  			if !keyFound {
   258  				err = unstructured.SetNestedField(obj.Object, nil, key)
   259  				if err != nil {
   260  					return err
   261  				}
   262  			}
   263  		}
   264  		return nil
   265  	})
   266  	if err != nil {
   267  		return err
   268  	}
   269  	y.objects = append(y.objects, *obj)
   270  
   271  	// Add an informational message (to mimic what you see on a kubectl apply)
   272  	group := obj.GetObjectKind().GroupVersionKind().Group
   273  	if len(group) > 0 {
   274  		group = fmt.Sprintf(".%s", group)
   275  	}
   276  	y.objectResultMsgs = append(y.objectResultMsgs, fmt.Sprintf("%s%s/%s %s", obj.GetKind(), group, obj.GetName(), string(result)))
   277  
   278  	return nil
   279  }
   280  
   281  // deleteAction deletes the object from the server
   282  func (y *YAMLApplier) deleteAction(obj *unstructured.Unstructured) error {
   283  	return y.execDeleteAction(obj, metav1.DeletePropagationOrphan)
   284  }
   285  
   286  // deleteAction deletes the object from the server
   287  func (y *YAMLApplier) deleteActionWithDependents(obj *unstructured.Unstructured) error {
   288  	return y.execDeleteAction(obj, metav1.DeletePropagationBackground)
   289  }
   290  
   291  func (y *YAMLApplier) execDeleteAction(obj *unstructured.Unstructured, propagationPolicy metav1.DeletionPropagation) error {
   292  	var ns = strings.TrimSpace(y.namespaceOverride)
   293  	if len(ns) > 0 {
   294  		obj.SetNamespace(ns)
   295  	}
   296  	deleteOptions := &crtpkg.DeleteOptions{
   297  		PropagationPolicy: &propagationPolicy,
   298  	}
   299  
   300  	if err := y.client.Delete(context.TODO(), obj, deleteOptions); err != nil {
   301  		if !errors.IsNotFound(err) {
   302  			return err
   303  		}
   304  	}
   305  	return nil
   306  }
   307  
   308  // doFileAction runs the action against a file
   309  func (y *YAMLApplier) doFileAction(filePath string, f action) error {
   310  	file, err := os.Open(filePath)
   311  	if err != nil {
   312  		return err
   313  	}
   314  	defer file.Close()
   315  	return y.doAction(bufio.NewReader(file), f)
   316  }
   317  
   318  // doStringAction runs the action against a string
   319  func (y *YAMLApplier) doStringAction(spec string, f action) error {
   320  	return y.doAction(bufio.NewReader(strings.NewReader(spec)), f)
   321  }
   322  
   323  // doTemplatedFileAction runs the action against a template file
   324  func (y *YAMLApplier) doTemplatedFileAction(filePath string, f action, args any) error {
   325  	templateName := path.Base(filePath)
   326  	tmpl, err := template.New(templateName).
   327  		Option("missingkey=error"). // Treat any missing keys as errors
   328  		Funcs(funcMap).
   329  		ParseFiles(filePath)
   330  	if err != nil {
   331  		return err
   332  	}
   333  	buffer := &bytes.Buffer{}
   334  	if err = tmpl.Execute(buffer, args); err != nil {
   335  		return err
   336  	}
   337  	return y.doAction(bufio.NewReader(buffer), f)
   338  }
   339  
   340  func (y *YAMLApplier) doTemplatedBytesAction(b []byte, f action, args any) error {
   341  	tmpl, err := template.New("bytetemplate").
   342  		Option("missingkey=error"). // Treat any missing keys as errors
   343  		Funcs(funcMap).
   344  		Parse(string(b))
   345  	if err != nil {
   346  		return err
   347  	}
   348  	buffer := &bytes.Buffer{}
   349  	if err = tmpl.Execute(buffer, args); err != nil {
   350  		return err
   351  	}
   352  	return y.doAction(bufio.NewReader(buffer), f)
   353  }
   354  
   355  // doAction executes the action on a YAML reader
   356  func (y *YAMLApplier) doAction(reader *bufio.Reader, f action) error {
   357  	objs, err := Unmarshall(reader)
   358  	if err != nil {
   359  		return err
   360  	}
   361  
   362  	for i := range objs {
   363  		if err := f(&objs[i]); err != nil {
   364  			return err
   365  		}
   366  	}
   367  	return nil
   368  }
   369  
   370  // Unmarshall a reader containing YAML to a list of unstructured objects
   371  func Unmarshall(reader *bufio.Reader) ([]unstructured.Unstructured, error) {
   372  	buffer := bytes.Buffer{}
   373  	objs := []unstructured.Unstructured{}
   374  
   375  	flushBuffer := func() error {
   376  		if buffer.Len() < 1 {
   377  			return nil
   378  		}
   379  		obj := unstructured.Unstructured{Object: map[string]interface{}{}}
   380  		yamlBytes := buffer.Bytes()
   381  		if err := yaml.Unmarshal(yamlBytes, &obj); err != nil {
   382  			return err
   383  		}
   384  		if len(obj.Object) > 0 {
   385  			objs = append(objs, obj)
   386  		}
   387  		buffer.Reset()
   388  		return nil
   389  	}
   390  
   391  	eofReached := false
   392  	for {
   393  		// Read the file line by line
   394  		line, err := reader.ReadBytes('\n')
   395  		if err != nil {
   396  			if err == io.EOF {
   397  				// EOF has been reached, but there may be some line data to process
   398  				eofReached = true
   399  			} else {
   400  				return objs, err
   401  			}
   402  		}
   403  		lineStr := string(line)
   404  		// Flush buffer at document break
   405  		if strings.TrimSpace(lineStr) == sep {
   406  			if err = flushBuffer(); err != nil {
   407  				return objs, err
   408  			}
   409  		} else {
   410  			// Save line to buffer
   411  			if !strings.HasPrefix(lineStr, "#") && len(strings.TrimSpace(lineStr)) > 0 {
   412  				if _, err := buffer.Write(line); err != nil {
   413  					return objs, err
   414  				}
   415  			}
   416  		}
   417  		// if EOF, flush the buffer and return the objs
   418  		if eofReached {
   419  			flushErr := flushBuffer()
   420  			return objs, flushErr
   421  		}
   422  	}
   423  }
   424  
   425  // merge keys from m2 into m1, overwriting existing keys of m1.
   426  func merge(m1, m2 map[string]interface{}) {
   427  	for k, v := range m2 {
   428  		m1[k] = v
   429  	}
   430  }
   431  
   432  // DeleteAll deletes all objects created by the applier
   433  // If you are using a YAMLApplier in a temporary context, please use defer y.DeleteAll()
   434  // to clean up resources when you are done.
   435  func (y *YAMLApplier) DeleteAll() error {
   436  	for i := range y.objects {
   437  		if err := y.client.Delete(context.TODO(), &y.objects[i]); err != nil {
   438  			if !errors.IsNotFound(err) {
   439  				return err
   440  			}
   441  		}
   442  	}
   443  
   444  	y.objects = []unstructured.Unstructured{}
   445  	return nil
   446  }
   447  
   448  // isYamlExt checks if a file has a YAML extension.
   449  func isYamlExt(fileName string) bool {
   450  	ext := path.Ext(fileName)
   451  	return ext == ".yml" || ext == ".yaml"
   452  }
   453  
   454  func filterYamlExt(files []os.DirEntry) []os.DirEntry {
   455  	res := []os.DirEntry{}
   456  	for _, file := range files {
   457  		if !file.IsDir() && isYamlExt(file.Name()) {
   458  			res = append(res, file)
   459  		}
   460  	}
   461  
   462  	return res
   463  }
   464  
   465  func nindent(indent int, s string) string {
   466  	spacing := strings.Repeat(" ", indent)
   467  	split := strings.FieldsFunc(s, func(r rune) bool {
   468  		switch r {
   469  		case '\n', '\v', '\f', '\r':
   470  			return true
   471  		default:
   472  			return false
   473  		}
   474  	})
   475  	sb := strings.Builder{}
   476  	for i := 0; i < len(split); i++ {
   477  		segment := split[i]
   478  		sb.WriteString(spacing)
   479  		sb.WriteString(strings.TrimSpace(segment))
   480  		if i < len(split)-1 {
   481  			sb.WriteRune('\n')
   482  		}
   483  	}
   484  
   485  	return sb.String()
   486  }
   487  
   488  func multiLineIndent(indentNum int, aff string) string {
   489  	var b = make([]byte, indentNum)
   490  	for i := 0; i < indentNum; i++ {
   491  		b[i] = 32
   492  	}
   493  	lines := strings.SplitAfter(aff, "\n")
   494  	for i, line := range lines {
   495  		lines[i] = string(b) + line
   496  	}
   497  	return strings.Join(lines[:], "")
   498  }