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

     1  // Copyright Contributors to the Open Cluster Management project
     2  package internal
     3  
     4  import (
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"github.com/dhaiducek/policy-generator-plugin/internal/expanders"
    15  	"github.com/dhaiducek/policy-generator-plugin/internal/types"
    16  	yaml "gopkg.in/yaml.v3"
    17  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    18  	"sigs.k8s.io/kustomize/api/krusty"
    19  	"sigs.k8s.io/kustomize/kyaml/filesys"
    20  )
    21  
    22  // getManifests will get all of the manifest files associated with the input policy configuration
    23  // separated by policyConf.Manifests entries. An error is returned if a manifest path cannot
    24  // be read.
    25  func getManifests(policyConf *types.PolicyConfig) ([][]map[string]interface{}, error) {
    26  	manifests := [][]map[string]interface{}{}
    27  	hasKustomize := map[string]bool{}
    28  
    29  	for _, manifest := range policyConf.Manifests {
    30  		manifestPaths := []string{}
    31  		manifestFiles := []map[string]interface{}{}
    32  		readErr := fmt.Errorf("failed to read the manifest path %s", manifest.Path)
    33  
    34  		manifestPathInfo, err := os.Stat(manifest.Path)
    35  		if err != nil {
    36  			return nil, readErr
    37  		}
    38  
    39  		resolvedFiles := []string{}
    40  
    41  		if manifestPathInfo.IsDir() {
    42  			files, err := os.ReadDir(manifest.Path)
    43  			if err != nil {
    44  				return nil, readErr
    45  			}
    46  
    47  			for _, f := range files {
    48  				if f.IsDir() {
    49  					continue
    50  				}
    51  
    52  				filepath := f.Name()
    53  				ext := path.Ext(filepath)
    54  
    55  				if ext != ".yaml" && ext != ".yml" {
    56  					continue
    57  				}
    58  				// Handle when a Kustomization directory is specified
    59  				_, filename := path.Split(filepath)
    60  				if filename == "kustomization.yml" || filename == "kustomization.yaml" {
    61  					hasKustomize[manifest.Path] = true
    62  					resolvedFiles = []string{manifest.Path}
    63  
    64  					break
    65  				}
    66  
    67  				yamlPath := path.Join(manifest.Path, f.Name())
    68  				resolvedFiles = append(resolvedFiles, yamlPath)
    69  			}
    70  
    71  			manifestPaths = append(manifestPaths, resolvedFiles...)
    72  		} else {
    73  			// Unmarshal the manifest in order to check for metadata patch replacement
    74  			manifestFile, err := unmarshalManifestFile(manifest.Path)
    75  			if err != nil {
    76  				return nil, err
    77  			}
    78  
    79  			if len(manifestFile) == 0 {
    80  				return nil, fmt.Errorf("found empty YAML in the manifest at %s", manifest.Path)
    81  			}
    82  			// Allowing replace the original manifest metadata.name and/or metadata.namespace if it is a single
    83  			// yaml structure in the manifest path
    84  			if len(manifestFile) == 1 && len(manifest.Patches) == 1 {
    85  				if patchMetadata, ok := manifest.Patches[0]["metadata"].(map[string]interface{}); ok {
    86  					if metadata, ok := manifestFile[0]["metadata"].(map[string]interface{}); ok {
    87  						name, ok := patchMetadata["name"].(string)
    88  						if ok && name != "" {
    89  							metadata["name"] = name
    90  						}
    91  						namespace, ok := patchMetadata["namespace"].(string)
    92  						if ok && namespace != "" {
    93  							metadata["namespace"] = namespace
    94  						}
    95  						manifestFile[0]["metadata"] = metadata
    96  					}
    97  				}
    98  			}
    99  
   100  			manifestFiles = append(manifestFiles, manifestFile...)
   101  		}
   102  
   103  		for _, manifestPath := range manifestPaths {
   104  			var manifestFile []map[string]interface{}
   105  			var err error
   106  
   107  			if hasKustomize[manifestPath] {
   108  				manifestFile, err = processKustomizeDir(manifestPath)
   109  			} else {
   110  				manifestFile, err = unmarshalManifestFile(manifestPath)
   111  			}
   112  
   113  			if err != nil {
   114  				return nil, err
   115  			}
   116  
   117  			if len(manifestFile) == 0 {
   118  				continue
   119  			}
   120  
   121  			manifestFiles = append(manifestFiles, manifestFile...)
   122  		}
   123  
   124  		if len(manifest.Patches) > 0 {
   125  			patcher := manifestPatcher{manifests: manifestFiles, patches: manifest.Patches}
   126  			const errTemplate = `failed to process the manifest at "%s": %w`
   127  
   128  			err = patcher.Validate()
   129  			if err != nil {
   130  				return nil, fmt.Errorf(errTemplate, manifest.Path, err)
   131  			}
   132  
   133  			patchedFiles, err := patcher.ApplyPatches()
   134  			if err != nil {
   135  				return nil, fmt.Errorf(errTemplate, manifest.Path, err)
   136  			}
   137  
   138  			manifestFiles = patchedFiles
   139  		}
   140  
   141  		manifests = append(manifests, manifestFiles)
   142  	}
   143  
   144  	return manifests, nil
   145  }
   146  
   147  // getPolicyTemplates generates the policy templates for the ConfigurationPolicy manifests
   148  // policyConf.ConsolidateManifests = true (default value) will generate a policy templates slice
   149  // that just has one template which includes all the manifests specified in policyConf.
   150  // policyConf.ConsolidateManifests = false will generate a policy templates slice
   151  // that each template includes a single manifest specified in policyConf.
   152  // An error is returned if one or more manifests cannot be read or are invalid.
   153  func getPolicyTemplates(policyConf *types.PolicyConfig) ([]map[string]interface{}, error) {
   154  	manifestGroups, err := getManifests(policyConf)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	objectTemplatesLength := len(manifestGroups)
   160  	policyTemplatesLength := 1
   161  
   162  	if !policyConf.ConsolidateManifests {
   163  		policyTemplatesLength = len(manifestGroups)
   164  		objectTemplatesLength = 0
   165  	}
   166  
   167  	objectTemplates := make([]map[string]interface{}, 0, objectTemplatesLength)
   168  	policyTemplates := make([]map[string]interface{}, 0, policyTemplatesLength)
   169  
   170  	for i, manifestGroup := range manifestGroups {
   171  		complianceType := policyConf.Manifests[i].ComplianceType
   172  		metadataComplianceType := policyConf.Manifests[i].MetadataComplianceType
   173  		ignorePending := policyConf.Manifests[i].IgnorePending
   174  		extraDeps := policyConf.Manifests[i].ExtraDependencies
   175  
   176  		for _, manifest := range manifestGroup {
   177  			isPolicyTypeManifest, isOcmPolicy, err := isPolicyTypeManifest(
   178  				manifest, policyConf.InformGatekeeperPolicies)
   179  			if err != nil {
   180  				return nil, fmt.Errorf(
   181  					"%w in manifest path: %s",
   182  					err,
   183  					policyConf.Manifests[i].Path,
   184  				)
   185  			}
   186  
   187  			if isPolicyTypeManifest {
   188  				policyTemplate := map[string]interface{}{"objectDefinition": manifest}
   189  
   190  				// Only set dependency options if it's an OCM policy
   191  				if isOcmPolicy {
   192  					setTemplateOptions(manifest, ignorePending, extraDeps)
   193  				}
   194  
   195  				policyTemplates = append(policyTemplates, policyTemplate)
   196  
   197  				continue
   198  			}
   199  
   200  			objTemplate := map[string]interface{}{
   201  				"complianceType":   complianceType,
   202  				"objectDefinition": manifest,
   203  			}
   204  
   205  			if metadataComplianceType != "" {
   206  				objTemplate["metadataComplianceType"] = metadataComplianceType
   207  			}
   208  
   209  			if policyConf.ConsolidateManifests {
   210  				// put all objTemplate with manifest into single consolidated objectTemplates
   211  				objectTemplates = append(objectTemplates, objTemplate)
   212  			} else {
   213  				// casting each objTemplate with manifest to objectTemplates type
   214  				// build policyTemplate for each objectTemplates
   215  				policyTemplate := buildPolicyTemplate(
   216  					policyConf,
   217  					len(policyTemplates)+1,
   218  					[]map[string]interface{}{objTemplate},
   219  					&policyConf.Manifests[i].ConfigurationPolicyOptions,
   220  				)
   221  
   222  				setTemplateOptions(policyTemplate, ignorePending, extraDeps)
   223  
   224  				policyTemplates = append(policyTemplates, policyTemplate)
   225  			}
   226  		}
   227  	}
   228  
   229  	if len(policyTemplates) == 0 && len(objectTemplates) == 0 {
   230  		return nil, fmt.Errorf(
   231  			"the policy %s must specify at least one non-empty manifest file", policyConf.Name,
   232  		)
   233  	}
   234  
   235  	// just build one policyTemplate by using the above non-empty consolidated objectTemplates
   236  	// ConsolidateManifests = true or there is non-policy-type manifest
   237  	if policyConf.ConsolidateManifests && len(objectTemplates) > 0 {
   238  		policyTemplate := buildPolicyTemplate(
   239  			policyConf,
   240  			1,
   241  			objectTemplates,
   242  			&policyConf.ConfigurationPolicyOptions,
   243  		)
   244  		setTemplateOptions(policyTemplate, policyConf.IgnorePending, policyConf.ExtraDependencies)
   245  		policyTemplates = append(policyTemplates, policyTemplate)
   246  	}
   247  
   248  	// check the enabled expanders and add additional policy templates
   249  	for i, manifestGroup := range manifestGroups {
   250  		ignorePending := policyConf.Manifests[i].IgnorePending
   251  		extraDeps := policyConf.Manifests[i].ExtraDependencies
   252  
   253  		for _, additionalTemplate := range handleExpanders(manifestGroup, *policyConf) {
   254  			setTemplateOptions(additionalTemplate, ignorePending, extraDeps)
   255  			policyTemplates = append(policyTemplates, additionalTemplate)
   256  		}
   257  	}
   258  
   259  	// order manifests now that everything is defined
   260  	if policyConf.OrderManifests {
   261  		previousTemplate := types.PolicyDependency{Compliance: "Compliant"}
   262  
   263  		for i, tmpl := range policyTemplates {
   264  			if previousTemplate.Name != "" {
   265  				policyTemplates[i]["extraDependencies"] = []types.PolicyDependency{previousTemplate}
   266  			}
   267  
   268  			// these fields are known to exist since the plugin created them
   269  			previousTemplate.Name, _, _ = unstructured.NestedString(tmpl, "objectDefinition", "metadata", "name")
   270  			previousTemplate.APIVersion, _, _ = unstructured.NestedString(tmpl, "objectDefinition", "apiVersion")
   271  			previousTemplate.Kind, _, _ = unstructured.NestedString(tmpl, "objectDefinition", "kind")
   272  		}
   273  	}
   274  
   275  	return policyTemplates, nil
   276  }
   277  
   278  func setTemplateOptions(tmpl map[string]interface{}, ignorePending bool, extraDeps []types.PolicyDependency) {
   279  	if ignorePending {
   280  		tmpl["ignorePending"] = ignorePending
   281  	}
   282  
   283  	if len(extraDeps) > 0 {
   284  		tmpl["extraDependencies"] = extraDeps
   285  	}
   286  }
   287  
   288  // isPolicyTypeManifest determines whether the manifest is a kind handled by the generator and
   289  // whether the manifest is a non-root OCM policy manifest by checking apiVersion and kind fields.
   290  // Return error when:
   291  // - apiVersion and kind fields can't be determined
   292  // - the manifest is a root policy manifest
   293  // - the manifest is invalid because it is missing a name
   294  func isPolicyTypeManifest(manifest map[string]interface{}, informGatekeeperPolicies bool) (bool, bool, error) {
   295  	apiVersion, found, err := unstructured.NestedString(manifest, "apiVersion")
   296  	if !found || err != nil {
   297  		return false, false, errors.New("invalid or not found apiVersion")
   298  	}
   299  
   300  	kind, found, err := unstructured.NestedString(manifest, "kind")
   301  	if !found || err != nil {
   302  		return false, false, errors.New("invalid or not found kind")
   303  	}
   304  
   305  	// Don't allow generation for root Policies
   306  	isOcmAPI := strings.HasPrefix(apiVersion, "policy.open-cluster-management.io")
   307  	if isOcmAPI && kind == "Policy" {
   308  		return false, false, errors.New("providing a root Policy kind is not supported by the generator; " +
   309  			"the manifest should be applied to the hub cluster directly")
   310  	}
   311  
   312  	// Identify OCM Policies
   313  	isOcmPolicy := isOcmAPI && kind != "Policy" && strings.HasSuffix(kind, "Policy")
   314  
   315  	// Identify Gatekeeper kinds
   316  	isGkConstraintTemplate := strings.HasPrefix(apiVersion, "templates.gatekeeper.sh") && kind == "ConstraintTemplate"
   317  	isGkConstraint := strings.HasPrefix(apiVersion, "constraints.gatekeeper.sh")
   318  	isGkObj := isGkConstraintTemplate || isGkConstraint
   319  
   320  	isPolicy := isOcmPolicy || (isGkObj && !informGatekeeperPolicies)
   321  
   322  	if isPolicy {
   323  		// metadata.name is required on policy manifests
   324  		_, found, err = unstructured.NestedString(manifest, "metadata", "name")
   325  		if !found || err != nil {
   326  			return isPolicy, isOcmPolicy, errors.New("invalid or not found metadata.name")
   327  		}
   328  	}
   329  
   330  	return isPolicy, isOcmPolicy, nil
   331  }
   332  
   333  // setNamespaceSelector sets the namespace selector, if set, on the input policy template.
   334  func setNamespaceSelector(
   335  	policyConf *types.ConfigurationPolicyOptions,
   336  	policyTemplate map[string]interface{},
   337  ) {
   338  	selector := policyConf.NamespaceSelector
   339  	if selector.Exclude != nil ||
   340  		selector.Include != nil ||
   341  		selector.MatchLabels != nil ||
   342  		selector.MatchExpressions != nil {
   343  		objDef := policyTemplate["objectDefinition"].(map[string]interface{})
   344  		spec := objDef["spec"].(map[string]interface{})
   345  		spec["namespaceSelector"] = selector
   346  	}
   347  }
   348  
   349  // processKustomizeDir runs a provided directory through Kustomize in order to generate the manifests within it.
   350  func processKustomizeDir(path string) ([]map[string]interface{}, error) {
   351  	k := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
   352  
   353  	resourceMap, err := k.Run(filesys.MakeFsOnDisk(), path)
   354  	if err != nil {
   355  		return nil, fmt.Errorf("failed to process provided kustomize directory '%s': %w", path, err)
   356  	}
   357  
   358  	manifestsYAML, err := resourceMap.AsYaml()
   359  	if err != nil {
   360  		return nil, fmt.Errorf("failed to convert the kustomize manifest(s) to YAML from directory '%s': %w", path, err)
   361  	}
   362  
   363  	manifests, err := unmarshalManifestBytes(manifestsYAML)
   364  	if err != nil {
   365  		return nil, fmt.Errorf("failed to read the kustomize manifest(s) from directory '%s': %w", path, err)
   366  	}
   367  
   368  	return manifests, nil
   369  }
   370  
   371  // buildPolicyTemplate generates single policy template by using objectTemplates with manifests.
   372  // policyNum defines which number the configuration policy is in the policy. If it is greater than
   373  // one then the configuration policy name will have policyNum appended to it.
   374  func buildPolicyTemplate(
   375  	policyConf *types.PolicyConfig,
   376  	policyNum int,
   377  	objectTemplates []map[string]interface{},
   378  	configPolicyOptionsOverrides *types.ConfigurationPolicyOptions,
   379  ) map[string]interface{} {
   380  	var name string
   381  	if policyNum > 1 {
   382  		name = fmt.Sprintf("%s%d", policyConf.Name, policyNum)
   383  	} else {
   384  		name = policyConf.Name
   385  	}
   386  
   387  	policyTemplate := map[string]interface{}{
   388  		"objectDefinition": map[string]interface{}{
   389  			"apiVersion": policyAPIVersion,
   390  			"kind":       configPolicyKind,
   391  			"metadata": map[string]interface{}{
   392  				"name": name,
   393  			},
   394  			"spec": map[string]interface{}{
   395  				"object-templates":  objectTemplates,
   396  				"remediationAction": policyConf.RemediationAction,
   397  				"severity":          policyConf.Severity,
   398  			},
   399  		},
   400  	}
   401  
   402  	// Set NamespaceSelector with policy configuration
   403  	setNamespaceSelector(&policyConf.ConfigurationPolicyOptions, policyTemplate)
   404  
   405  	if len(policyConf.ConfigurationPolicyAnnotations) > 0 {
   406  		objDef := policyTemplate["objectDefinition"].(map[string]interface{})
   407  		metadata := objDef["metadata"].(map[string]interface{})
   408  		metadata["annotations"] = policyConf.ConfigurationPolicyAnnotations
   409  	}
   410  
   411  	objDef := policyTemplate["objectDefinition"].(map[string]interface{})
   412  	configSpec := objDef["spec"].(map[string]interface{})
   413  
   414  	// Set EvaluationInterval with manifest overrides
   415  	evaluationInterval := configPolicyOptionsOverrides.EvaluationInterval
   416  	if evaluationInterval.Compliant != "" || evaluationInterval.NonCompliant != "" {
   417  		evalInterval := map[string]interface{}{}
   418  
   419  		if evaluationInterval.Compliant != "" {
   420  			evalInterval["compliant"] = evaluationInterval.Compliant
   421  		}
   422  
   423  		if evaluationInterval.NonCompliant != "" {
   424  			evalInterval["noncompliant"] = evaluationInterval.NonCompliant
   425  		}
   426  
   427  		configSpec["evaluationInterval"] = evalInterval
   428  	}
   429  
   430  	// Set NamespaceSelector with manifest overrides
   431  	setNamespaceSelector(configPolicyOptionsOverrides, policyTemplate)
   432  
   433  	// Set PruneObjectBehavior with manifest overrides
   434  	if configPolicyOptionsOverrides.PruneObjectBehavior != "" {
   435  		configSpec["pruneObjectBehavior"] = configPolicyOptionsOverrides.PruneObjectBehavior
   436  	}
   437  
   438  	// Set RemediationAction with manifest overrides
   439  	if configPolicyOptionsOverrides.RemediationAction != "" {
   440  		configSpec["remediationAction"] = configPolicyOptionsOverrides.RemediationAction
   441  	}
   442  
   443  	// Set Severity with manifest overrides
   444  	if configPolicyOptionsOverrides.Severity != "" {
   445  		configSpec["severity"] = configPolicyOptionsOverrides.Severity
   446  	}
   447  
   448  	return policyTemplate
   449  }
   450  
   451  // handleExpanders will go through all the enabled expanders and generate additional
   452  // policy templates to include in the policy.
   453  func handleExpanders(manifests []map[string]interface{}, policyConf types.PolicyConfig) []map[string]interface{} {
   454  	policyTemplates := []map[string]interface{}{}
   455  
   456  	for _, expander := range expanders.GetExpanders() {
   457  		for _, m := range manifests {
   458  			if expander.Enabled(&policyConf) && expander.CanHandle(m) {
   459  				expandedPolicyTemplates := expander.Expand(m, policyConf.Severity)
   460  				policyTemplates = append(policyTemplates, expandedPolicyTemplates...)
   461  			}
   462  		}
   463  	}
   464  
   465  	return policyTemplates
   466  }
   467  
   468  // unmarshalManifestFile unmarshals the input object manifest/definition file into
   469  // a slice in order to account for multiple YAML documents in the same file.
   470  // If the file cannot be decoded or each document is not a map, an error will
   471  // be returned.
   472  func unmarshalManifestFile(manifestPath string) ([]map[string]interface{}, error) {
   473  	// #nosec G304
   474  	manifestBytes, err := os.ReadFile(manifestPath)
   475  	if err != nil {
   476  		return nil, fmt.Errorf("failed to read the manifest file %s", manifestPath)
   477  	}
   478  
   479  	rv, err := unmarshalManifestBytes(manifestBytes)
   480  	if err != nil {
   481  		return nil, fmt.Errorf("failed to decode the manifest file at %s: %w", manifestPath, err)
   482  	}
   483  
   484  	return rv, nil
   485  }
   486  
   487  // unmarshalManifestBytes unmarshals the input bytes slice of an object manifest/definition file
   488  // into a slice of maps in order to account for multiple YAML documents in the bytes slice. If each
   489  // document is not a map, an error will be returned.
   490  func unmarshalManifestBytes(manifestBytes []byte) ([]map[string]interface{}, error) {
   491  	yamlDocs := []map[string]interface{}{}
   492  	d := yaml.NewDecoder(bytes.NewReader(manifestBytes))
   493  
   494  	for {
   495  		var obj interface{}
   496  
   497  		err := d.Decode(&obj)
   498  		if err != nil {
   499  			if errors.Is(err, io.EOF) {
   500  				break
   501  			}
   502  
   503  			//nolint:wrapcheck
   504  			return nil, err
   505  		}
   506  
   507  		if _, ok := obj.(map[string]interface{}); !ok && obj != nil {
   508  			err := errors.New("the input manifests must be in the format of YAML objects")
   509  
   510  			return nil, err
   511  		}
   512  
   513  		if obj != nil {
   514  			yamlDocs = append(yamlDocs, obj.(map[string]interface{}))
   515  		}
   516  	}
   517  
   518  	return yamlDocs, nil
   519  }
   520  
   521  // verifyManifestPath verifies that the manifest path is in the directory tree under baseDirectory.
   522  // An error is returned if it is not or the paths couldn't be properly resolved.
   523  func verifyManifestPath(baseDirectory string, manifestPath string) error {
   524  	absPath, err := filepath.Abs(manifestPath)
   525  	if err != nil {
   526  		return fmt.Errorf("could not resolve the manifest path %s to an absolute path", manifestPath)
   527  	}
   528  
   529  	absPath, err = filepath.EvalSymlinks(absPath)
   530  	if err != nil {
   531  		return fmt.Errorf("could not resolve symlinks to the manifest path %s", manifestPath)
   532  	}
   533  
   534  	relPath, err := filepath.Rel(baseDirectory, absPath)
   535  	if err != nil {
   536  		return fmt.Errorf(
   537  			"could not resolve the manifest path %s to a relative path from the kustomization.yaml file",
   538  			manifestPath,
   539  		)
   540  	}
   541  
   542  	if relPath == "." {
   543  		return fmt.Errorf(
   544  			"the manifest path %s may not refer to the same directory as the kustomization.yaml file",
   545  			manifestPath,
   546  		)
   547  	}
   548  
   549  	parDir := ".." + string(filepath.Separator)
   550  	if strings.HasPrefix(relPath, parDir) || relPath == ".." {
   551  		return fmt.Errorf(
   552  			"the manifest path %s is not in the same directory tree as the kustomization.yaml file",
   553  			manifestPath,
   554  		)
   555  	}
   556  
   557  	return nil
   558  }
   559  
   560  // Check policy-templates to see if all the remediation actions match, if so return the root policy remediation action
   561  func getRootRemediationAction(policyTemplates []map[string]interface{}) string {
   562  	var action string
   563  
   564  	for _, value := range policyTemplates {
   565  		objDef := value["objectDefinition"].(map[string]interface{})
   566  		if spec, ok := objDef["spec"].(map[string]interface{}); ok {
   567  			if _, ok = spec["remediationAction"].(string); ok {
   568  				if action == "" {
   569  					action = spec["remediationAction"].(string)
   570  				} else if spec["remediationAction"].(string) != action {
   571  					return ""
   572  				}
   573  			}
   574  		}
   575  	}
   576  
   577  	return action
   578  }