mirror of
https://github.com/kyverno/kyverno.git
synced 2025-01-20 18:52:16 +00:00
288c9091ec
Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com> Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
431 lines
13 KiB
Go
431 lines
13 KiB
Go
package utils
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
"github.com/go-logr/logr"
|
|
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
|
|
"github.com/kyverno/kyverno/pkg/clients/dclient"
|
|
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
|
|
"github.com/kyverno/kyverno/pkg/logging"
|
|
wildcard "github.com/kyverno/kyverno/pkg/utils/wildcard"
|
|
"github.com/pkg/errors"
|
|
admissionv1 "k8s.io/api/admission/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/util/runtime"
|
|
"k8s.io/client-go/discovery"
|
|
)
|
|
|
|
var regexVersion = regexp.MustCompile(`v(\d+).(\d+).(\d+)\.*`)
|
|
|
|
// CopyMap creates a full copy of the target map
|
|
func CopyMap(m map[string]interface{}) map[string]interface{} {
|
|
mapCopy := make(map[string]interface{})
|
|
for k, v := range m {
|
|
mapCopy[k] = v
|
|
}
|
|
|
|
return mapCopy
|
|
}
|
|
|
|
// CopySliceOfMaps creates a full copy of the target slice
|
|
func CopySliceOfMaps(s []map[string]interface{}) []interface{} {
|
|
sliceCopy := make([]interface{}, len(s))
|
|
for i, v := range s {
|
|
sliceCopy[i] = CopyMap(v)
|
|
}
|
|
|
|
return sliceCopy
|
|
}
|
|
|
|
func ToMap(data interface{}) (map[string]interface{}, error) {
|
|
if m, ok := data.(map[string]interface{}); ok {
|
|
return m, nil
|
|
}
|
|
|
|
b, err := json.Marshal(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mapData := make(map[string]interface{})
|
|
err = json.Unmarshal(b, &mapData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return mapData, nil
|
|
}
|
|
|
|
// Contains checks if a string is contained in a list of string
|
|
func contains(list []string, element string, fn func(string, string) bool) bool {
|
|
for _, e := range list {
|
|
if fn(e, element) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ContainsNamepace check if namespace satisfies any list of pattern(regex)
|
|
func ContainsNamepace(patterns []string, ns string) bool {
|
|
return contains(patterns, ns, comparePatterns)
|
|
}
|
|
|
|
func ContainsWildcardPatterns(patterns []string, key string) bool {
|
|
return contains(patterns, key, comparePatterns)
|
|
}
|
|
|
|
func comparePatterns(pattern, ns string) bool {
|
|
return wildcard.Match(pattern, ns)
|
|
}
|
|
|
|
// CRDsInstalled checks if the Kyverno CRDs are installed or not
|
|
func CRDsInstalled(discovery dclient.IDiscovery) bool {
|
|
kyvernoCRDs := []string{"ClusterPolicy", "ClusterPolicyReport", "PolicyReport", "AdmissionReport", "BackgroundScanReport", "ClusterAdmissionReport", "ClusterBackgroundScanReport"}
|
|
for _, crd := range kyvernoCRDs {
|
|
if !isCRDInstalled(discovery, crd) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func isCRDInstalled(discoveryClient dclient.IDiscovery, kind string) bool {
|
|
gvr, err := discoveryClient.GetGVRFromKind(kind)
|
|
if gvr.Empty() {
|
|
if err == nil {
|
|
err = fmt.Errorf("not found")
|
|
}
|
|
logging.Error(err, "failed to retrieve CRD", "kind", kind)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ExtractResources extracts the new and old resource as unstructured
|
|
func ExtractResources(newRaw []byte, request *admissionv1.AdmissionRequest) (unstructured.Unstructured, unstructured.Unstructured, error) {
|
|
var emptyResource unstructured.Unstructured
|
|
var newResource unstructured.Unstructured
|
|
var oldResource unstructured.Unstructured
|
|
var err error
|
|
|
|
// New Resource
|
|
if newRaw == nil {
|
|
newRaw = request.Object.Raw
|
|
}
|
|
|
|
if newRaw != nil {
|
|
newResource, err = ConvertResource(newRaw, request.Kind.Group, request.Kind.Version, request.Kind.Kind, request.Namespace)
|
|
if err != nil {
|
|
return emptyResource, emptyResource, fmt.Errorf("failed to convert new raw to unstructured: %v", err)
|
|
}
|
|
}
|
|
|
|
// Old Resource
|
|
oldRaw := request.OldObject.Raw
|
|
if oldRaw != nil {
|
|
oldResource, err = ConvertResource(oldRaw, request.Kind.Group, request.Kind.Version, request.Kind.Kind, request.Namespace)
|
|
if err != nil {
|
|
return emptyResource, emptyResource, fmt.Errorf("failed to convert old raw to unstructured: %v", err)
|
|
}
|
|
}
|
|
|
|
return newResource, oldResource, err
|
|
}
|
|
|
|
// ConvertResource converts raw bytes to an unstructured object
|
|
func ConvertResource(raw []byte, group, version, kind, namespace string) (unstructured.Unstructured, error) {
|
|
obj, err := engineutils.ConvertToUnstructured(raw)
|
|
if err != nil {
|
|
return unstructured.Unstructured{}, fmt.Errorf("failed to convert raw to unstructured: %v", err)
|
|
}
|
|
|
|
obj.SetGroupVersionKind(schema.GroupVersionKind{Group: group, Version: version, Kind: kind})
|
|
|
|
if namespace != "" && kind != "Namespace" {
|
|
obj.SetNamespace(namespace)
|
|
}
|
|
|
|
if obj.GetKind() == "Namespace" && obj.GetNamespace() != "" {
|
|
obj.SetNamespace("")
|
|
}
|
|
|
|
return *obj, nil
|
|
}
|
|
|
|
func NormalizeSecret(resource *unstructured.Unstructured) (unstructured.Unstructured, error) {
|
|
var secret corev1.Secret
|
|
data, err := json.Marshal(resource.Object)
|
|
if err != nil {
|
|
return *resource, err
|
|
}
|
|
err = json.Unmarshal(data, &secret)
|
|
if err != nil {
|
|
return *resource, errors.Wrap(err, "object unable to convert to secret")
|
|
}
|
|
for k, v := range secret.Data {
|
|
if len(v) == 0 {
|
|
secret.Data[k] = []byte("")
|
|
}
|
|
}
|
|
updateSecret := map[string]interface{}{}
|
|
raw, err := json.Marshal(&secret)
|
|
if err != nil {
|
|
return *resource, nil
|
|
}
|
|
|
|
err = json.Unmarshal(raw, &updateSecret)
|
|
if err != nil {
|
|
return *resource, nil
|
|
}
|
|
|
|
if err != nil {
|
|
return *resource, errors.Wrap(err, "object unable to convert from secret")
|
|
}
|
|
if secret.Data != nil {
|
|
err = unstructured.SetNestedMap(resource.Object, updateSecret["data"].(map[string]interface{}), "data")
|
|
if err != nil {
|
|
return *resource, errors.Wrap(err, "failed to set secret.data")
|
|
}
|
|
}
|
|
return *resource, nil
|
|
}
|
|
|
|
// RedactSecret masks keys of data and metadata.annotation fields of Secrets.
|
|
func RedactSecret(resource *unstructured.Unstructured) (unstructured.Unstructured, error) {
|
|
var secret *corev1.Secret
|
|
data, err := json.Marshal(resource.Object)
|
|
if err != nil {
|
|
return *resource, err
|
|
}
|
|
err = json.Unmarshal(data, &secret)
|
|
if err != nil {
|
|
return *resource, errors.Wrap(err, "unable to convert object to secret")
|
|
}
|
|
stringSecret := struct {
|
|
Data map[string]string `json:"string_data"`
|
|
*corev1.Secret
|
|
}{
|
|
Data: make(map[string]string),
|
|
Secret: secret,
|
|
}
|
|
for key := range secret.Data {
|
|
secret.Data[key] = []byte("**REDACTED**")
|
|
stringSecret.Data[key] = string(secret.Data[key])
|
|
}
|
|
for key := range secret.Annotations {
|
|
secret.Annotations[key] = "**REDACTED**"
|
|
}
|
|
updateSecret := map[string]interface{}{}
|
|
raw, err := json.Marshal(stringSecret)
|
|
if err != nil {
|
|
return *resource, nil
|
|
}
|
|
err = json.Unmarshal(raw, &updateSecret)
|
|
if err != nil {
|
|
return *resource, errors.Wrap(err, "unable to convert object from secret")
|
|
}
|
|
if secret.Data != nil {
|
|
v := updateSecret["string_data"].(map[string]interface{})
|
|
err = unstructured.SetNestedMap(resource.Object, v, "data")
|
|
if err != nil {
|
|
return *resource, errors.Wrap(err, "failed to set secret.data")
|
|
}
|
|
}
|
|
if secret.Annotations != nil {
|
|
metadata, err := ToMap(resource.Object["metadata"])
|
|
if err != nil {
|
|
return *resource, errors.Wrap(err, "unable to convert metadata to map")
|
|
}
|
|
updatedMeta := updateSecret["metadata"].(map[string]interface{})
|
|
if err != nil {
|
|
return *resource, errors.Wrap(err, "unable to convert object from secret")
|
|
}
|
|
err = unstructured.SetNestedMap(metadata, updatedMeta["annotations"].(map[string]interface{}), "annotations")
|
|
if err != nil {
|
|
return *resource, errors.Wrap(err, "failed to set secret.annotations")
|
|
}
|
|
}
|
|
return *resource, nil
|
|
}
|
|
|
|
// HigherThanKubernetesVersion compare Kubernetes client version to user given version
|
|
func HigherThanKubernetesVersion(client discovery.ServerVersionInterface, log logr.Logger, major, minor, patch int) bool {
|
|
logger := log.WithName("CompareKubernetesVersion")
|
|
serverVersion, err := client.ServerVersion()
|
|
if err != nil {
|
|
logger.Error(err, "Failed to get kubernetes server version")
|
|
return false
|
|
}
|
|
|
|
b, err := isVersionHigher(serverVersion.String(), major, minor, patch)
|
|
if err != nil {
|
|
logger.Error(err, "serverVersion", serverVersion.String())
|
|
return false
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
func isVersionHigher(version string, major int, minor int, patch int) (bool, error) {
|
|
groups := regexVersion.FindStringSubmatch(version)
|
|
if len(groups) != 4 {
|
|
return false, fmt.Errorf("invalid version %s. Expected {major}.{minor}.{patch}", version)
|
|
}
|
|
|
|
currentMajor, err := strconv.Atoi(groups[1])
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to extract major version from %s", version)
|
|
}
|
|
|
|
currentMinor, err := strconv.Atoi(groups[2])
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to extract minor version from %s", version)
|
|
}
|
|
|
|
currentPatch, err := strconv.Atoi(groups[3])
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to extract minor version from %s", version)
|
|
}
|
|
|
|
if currentMajor < major ||
|
|
(currentMajor == major && currentMinor < minor) ||
|
|
(currentMajor == major && currentMinor == minor && currentPatch <= patch) {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// SliceContains checks whether values are contained in slice
|
|
func SliceContains(slice []string, values ...string) bool {
|
|
sliceElementsMap := make(map[string]bool, len(slice))
|
|
for _, sliceElement := range slice {
|
|
sliceElementsMap[sliceElement] = true
|
|
}
|
|
for _, value := range values {
|
|
if sliceElementsMap[value] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ApiextensionsJsonToKyvernoConditions takes in user-provided conditions in abstract apiextensions.JSON form
|
|
// and converts it into []kyverno.Condition or kyverno.AnyAllConditions according to its content.
|
|
// it also helps in validating the condtions as it returns an error when the conditions are provided wrongfully by the user.
|
|
func ApiextensionsJsonToKyvernoConditions(original apiextensions.JSON) (interface{}, error) {
|
|
path := "preconditions/validate.deny.conditions"
|
|
|
|
// checks for the existence any other field apart from 'any'/'all' under preconditions/validate.deny.conditions
|
|
unknownFieldChecker := func(jsonByteArr []byte, path string) error {
|
|
allowedKeys := map[string]bool{
|
|
"any": true,
|
|
"all": true,
|
|
}
|
|
var jsonDecoded map[string]interface{}
|
|
if err := json.Unmarshal(jsonByteArr, &jsonDecoded); err != nil {
|
|
return fmt.Errorf("error occurred while checking for unknown fields under %s: %+v", path, err)
|
|
}
|
|
for k := range jsonDecoded {
|
|
if !allowedKeys[k] {
|
|
return fmt.Errorf("unknown field '%s' found under %s", k, path)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// marshalling the abstract apiextensions.JSON back to JSON form
|
|
jsonByte, err := json.Marshal(original)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error occurred while marshalling %s: %+v", path, err)
|
|
}
|
|
|
|
var kyvernoOldConditions []kyvernov1.Condition
|
|
if err = json.Unmarshal(jsonByte, &kyvernoOldConditions); err == nil {
|
|
var validConditionOperator bool
|
|
|
|
for _, jsonOp := range kyvernoOldConditions {
|
|
for _, validOp := range kyvernov1.ConditionOperators {
|
|
if jsonOp.Operator == validOp {
|
|
validConditionOperator = true
|
|
}
|
|
}
|
|
if !validConditionOperator {
|
|
return nil, fmt.Errorf("invalid condition operator: %s", jsonOp.Operator)
|
|
}
|
|
validConditionOperator = false
|
|
}
|
|
|
|
return kyvernoOldConditions, nil
|
|
}
|
|
|
|
var kyvernoAnyAllConditions kyvernov1.AnyAllConditions
|
|
if err = json.Unmarshal(jsonByte, &kyvernoAnyAllConditions); err == nil {
|
|
// checking if unknown fields exist or not
|
|
err = unknownFieldChecker(jsonByte, path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error occurred while parsing %s: %+v", path, err)
|
|
}
|
|
return kyvernoAnyAllConditions, nil
|
|
}
|
|
return nil, fmt.Errorf("error occurred while parsing %s: %+v", path, err)
|
|
}
|
|
|
|
func OverrideRuntimeErrorHandler() {
|
|
logger := logging.WithName("RuntimeErrorHandler")
|
|
if len(runtime.ErrorHandlers) > 0 {
|
|
runtime.ErrorHandlers[0] = func(err error) {
|
|
logger.V(6).Info("runtime error", "msg", err.Error())
|
|
}
|
|
} else {
|
|
runtime.ErrorHandlers = []func(err error){
|
|
func(err error) {
|
|
logger.V(6).Info("runtime error", "msg", err.Error())
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
func SeperateWildcards(l []string) (lw []string, rl []string) {
|
|
for _, val := range l {
|
|
if wildcard.ContainsWildcard(val) {
|
|
lw = append(lw, val)
|
|
} else {
|
|
rl = append(rl, val)
|
|
}
|
|
}
|
|
return lw, rl
|
|
}
|
|
|
|
func CheckWildcardNamespaces(patterns []string, ns []string) (string, string, bool) {
|
|
for _, n := range ns {
|
|
pat, element, boolval := containsNamespaceWithStringReturn(patterns, n)
|
|
if boolval {
|
|
return pat, element, true
|
|
}
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
func containsWithStringReturn(list []string, element string, fn func(string, string) bool) (string, string, bool) {
|
|
for _, e := range list {
|
|
if fn(e, element) {
|
|
return e, element, true
|
|
}
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
// containsNamespaceWithStringReturn check if namespace satisfies any list of pattern(regex)
|
|
func containsNamespaceWithStringReturn(patterns []string, ns string) (string, string, bool) {
|
|
return containsWithStringReturn(patterns, ns, comparePatterns)
|
|
}
|