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 }