github.com/dhaiducek/policy-generator-plugin@v1.99.99/internal/patches.go (about)

     1  // Copyright Contributors to the Open Cluster Management project
     2  package internal
     3  
     4  import (
     5  	"errors"
     6  	"fmt"
     7  	"path"
     8  
     9  	yaml "gopkg.in/yaml.v3"
    10  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    11  	"sigs.k8s.io/kustomize/api/krusty"
    12  	"sigs.k8s.io/kustomize/kyaml/filesys"
    13  )
    14  
    15  type manifestPatcher struct {
    16  	// The manifests to patch.
    17  	manifests []map[string]interface{}
    18  	// The Kustomize patches to apply on the manifests. Note that modifications are made
    19  	// to the input maps. If this is an issue, provide a deep copy of the patches.
    20  	patches []map[string]interface{}
    21  }
    22  
    23  // validateManifestInfo verifies that the apiVersion, kind, metadata.name fields from a manifest
    24  // are set. If at least one is not present, an error is returned based on the input error template
    25  // which accepts the field name.
    26  func validateManifestInfo(manifest map[string]interface{}, errTemplate string) error {
    27  	apiVersion, _, _ := unstructured.NestedString(manifest, "apiVersion")
    28  	if apiVersion == "" {
    29  		return fmt.Errorf(errTemplate, "apiVersion")
    30  	}
    31  
    32  	kind, _, _ := unstructured.NestedString(manifest, "kind")
    33  	if kind == "" {
    34  		return fmt.Errorf(errTemplate, "kind")
    35  	}
    36  
    37  	name, _, _ := unstructured.NestedString(manifest, "metadata", "name")
    38  	if name == "" {
    39  		return fmt.Errorf(errTemplate, "metadata.name")
    40  	}
    41  
    42  	return nil
    43  }
    44  
    45  // Validate performs basic validatation of the manifests and patches. Any missing values in the
    46  // patches that can be derived are added to the patches. An error is returned if a manifest or
    47  // patch is invalid.
    48  func (m *manifestPatcher) Validate() error {
    49  	if len(m.manifests) == 0 {
    50  		return errors.New("there must be one or more manifests")
    51  	}
    52  
    53  	// Validate the manifest fields for applying patches
    54  	const errTemplate = `all manifests must have the "%s" field set to a non-empty string`
    55  	for _, manifest := range m.manifests {
    56  		err := validateManifestInfo(manifest, errTemplate)
    57  		if err != nil {
    58  			return err
    59  		}
    60  	}
    61  
    62  	// If there is more than a single manifest, the patch must contain a name, kind, and apiVersion.
    63  	if len(m.manifests) > 1 {
    64  		const patchErrTemplate = `patches must have the "%s" field set to a non-empty string ` +
    65  			`when there is more than one manifest it can apply to`
    66  
    67  		for _, patch := range m.patches {
    68  			err := validateManifestInfo(patch, patchErrTemplate)
    69  			if err != nil {
    70  				return err
    71  			}
    72  		}
    73  
    74  		// At this point, there is a reasonable chance that the patch is valid. Kustomize can handle
    75  		// further validation.
    76  		return nil
    77  	}
    78  
    79  	// At this point, we know we are only dealing with a single manifest, so we can assume all
    80  	// the patches are meant to apply to it.
    81  	manifest := (m.manifests)[0]
    82  	// The following fields have already been confirmed to exist and be valid previously in this
    83  	// method.
    84  	apiVersion, _, _ := unstructured.NestedString(manifest, "apiVersion")
    85  	kind, _, _ := unstructured.NestedString(manifest, "kind")
    86  	name, _, _ := unstructured.NestedString(manifest, "metadata", "name")
    87  	// The namespace would only apply to a manifest for a namespaced resource, so this is optional.
    88  	// Treat an empty string as meaning the manifest is for a cluster-wide resource.
    89  	namespace, _, _ := unstructured.NestedString(manifest, "metadata", "namespace")
    90  
    91  	// Apply defaults on the patches
    92  	for i := range m.patches {
    93  		err := setPatchDefaults(apiVersion, kind, name, namespace, m.patches[i])
    94  		if err != nil {
    95  			return err
    96  		}
    97  	}
    98  
    99  	return nil
   100  }
   101  
   102  // setPatchDefaults is a helper function for Validate that sets any missing values on the patches
   103  // that can be derived. An error is returned if a patch is in an invalid format.
   104  func setPatchDefaults(
   105  	apiVersion, kind, name, namespace string, patch map[string]interface{},
   106  ) error {
   107  	errTemplate := `failed to retrieve the "%s" field from the manifest of name "` + name +
   108  		`"` + ` and kind "` + kind + `": %v`
   109  	setErrTemplate := `failed to set the "%s" field on the patch from the manifest of name "` +
   110  		name + `"` + ` and kind "` + kind + `": %v`
   111  
   112  	patchAPIVersion, _, err := unstructured.NestedString(patch, "apiVersion")
   113  	if err != nil {
   114  		return fmt.Errorf(errTemplate, "apiVersion", err)
   115  	}
   116  
   117  	if patchAPIVersion == "" {
   118  		err = unstructured.SetNestedField(patch, apiVersion, "apiVersion")
   119  		if err != nil {
   120  			return fmt.Errorf(setErrTemplate, "apiVersion", err)
   121  		}
   122  	}
   123  
   124  	patchKind, _, err := unstructured.NestedString(patch, "kind")
   125  	if err != nil {
   126  		return fmt.Errorf(errTemplate, "kind", err)
   127  	}
   128  
   129  	if patchKind == "" {
   130  		err = unstructured.SetNestedField(patch, kind, "kind")
   131  		if err != nil {
   132  			return fmt.Errorf(setErrTemplate, "kind", err)
   133  		}
   134  	}
   135  
   136  	patchName, _, err := unstructured.NestedString(patch, "metadata", "name")
   137  	if err != nil {
   138  		return fmt.Errorf(errTemplate, "metadata.name", err)
   139  	}
   140  
   141  	if patchName == "" {
   142  		err = unstructured.SetNestedField(patch, name, "metadata", "name")
   143  		if err != nil {
   144  			return fmt.Errorf(setErrTemplate, "metadata.name", err)
   145  		}
   146  	}
   147  
   148  	patchNamespace, _, err := unstructured.NestedString(patch, "metadata", "namespace")
   149  	if err != nil {
   150  		return fmt.Errorf(errTemplate, "metadata.namespace", err)
   151  	}
   152  
   153  	if patchNamespace == "" {
   154  		err = unstructured.SetNestedField(patch, namespace, "metadata", "namespace")
   155  		if err != nil {
   156  			return fmt.Errorf(setErrTemplate, "metadata.namespace", err)
   157  		}
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  // ApplyPatches applies the Kustomize patches on the input manifests using Kustomize and returns
   164  // the patched manifests. An error is returned if the patches can't be applied. This should be
   165  // run after the Validate method.
   166  func (m *manifestPatcher) ApplyPatches() ([]map[string]interface{}, error) {
   167  	kustomizeDir := "kustomize"
   168  
   169  	// Create the file system in memory with the Kustomize YAML files
   170  	fSys := filesys.MakeFsInMemory()
   171  
   172  	err := fSys.Mkdir(kustomizeDir)
   173  	if err != nil {
   174  		return nil, fmt.Errorf("an unexpected error occurred when configuring Kustomize: %w", err)
   175  	}
   176  
   177  	kustomizationYAMLFile := map[string][]interface{}{
   178  		"resources":             {},
   179  		"patchesStrategicMerge": {},
   180  	}
   181  
   182  	options := []struct {
   183  		optionType   string
   184  		kustomizeKey string
   185  		objects      []map[string]interface{}
   186  	}{
   187  		{"manifest", "resources", m.manifests},
   188  		{"patch", "patchesStrategicMerge", m.patches},
   189  	}
   190  	for _, option := range options {
   191  		for i, object := range option.objects {
   192  			var objectYAML []byte
   193  			objectYAML, err := yaml.Marshal(object)
   194  			const errTemplate = "an unexpected error occurred when converting the %s back to " +
   195  				"YAML: %w"
   196  
   197  			if err != nil {
   198  				return nil, fmt.Errorf(errTemplate, option.optionType, err)
   199  			}
   200  
   201  			manifestFileName := fmt.Sprintf("%s%d.yaml", option.optionType, i)
   202  
   203  			err = fSys.WriteFile(path.Join(kustomizeDir, manifestFileName), objectYAML)
   204  			if err != nil {
   205  				return nil, fmt.Errorf(errTemplate, option.optionType, err)
   206  			}
   207  
   208  			kustomizationYAMLFile[option.kustomizeKey] = append(
   209  				kustomizationYAMLFile[option.kustomizeKey], manifestFileName,
   210  			)
   211  		}
   212  	}
   213  
   214  	var kustomizationYAML []byte
   215  	kustomizationYAML, err = yaml.Marshal(kustomizationYAMLFile)
   216  	const errTemplate = "an unexpected error occurred when creating the kustomization.yaml file: %w"
   217  
   218  	if err != nil {
   219  		return nil, fmt.Errorf(errTemplate, err)
   220  	}
   221  
   222  	err = fSys.WriteFile(path.Join(kustomizeDir, "kustomization.yaml"), kustomizationYAML)
   223  	if err != nil {
   224  		return nil, fmt.Errorf(errTemplate, err)
   225  	}
   226  
   227  	k := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
   228  
   229  	resMap, err := k.Run(fSys, "kustomize")
   230  	if err != nil {
   231  		err = fmt.Errorf(
   232  			"failed to apply the patch(es) to the manifest(s) using Kustomize: %w", err,
   233  		)
   234  
   235  		return nil, err
   236  	}
   237  
   238  	manifestsYAML, err := resMap.AsYaml()
   239  	if err != nil {
   240  		return nil, fmt.Errorf("failed to convert the patched manifest(s) back to YAML: %w", err)
   241  	}
   242  
   243  	manifests, err := unmarshalManifestBytes(manifestsYAML)
   244  	if err != nil {
   245  		return nil, fmt.Errorf("failed to read the patched manifest(s): %w", err)
   246  	}
   247  
   248  	return manifests, nil
   249  }