1 package runtime 2 3 import ( 4 "errors" 5 "regexp" 6 7 kerrors "k8s.io/apimachinery/pkg/api/errors" 8 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 ) 10 11 // Match CEL and known custom webhook immutable error variants. 12 var matchImmutableFieldErrors = []*regexp.Regexp{ 13 regexp.MustCompile(`.*is\simmutable.*`), 14 regexp.MustCompile(`.*immutable\sfield.*`), // KCC famous deadhooks 15 } 16 17 // IsImmutableError checks if the given error indicates that a recoverable 18 // immutability error has been encountered, which occurs when an object failed 19 // to be applied strictly due to updating immutable fields. If there are 20 // additional causes detected (e.g., the error is caused both by an immutable 21 // field being changed and another field being invalid), the immutability error 22 // is considered non-recoverable and this function returns false. This is 23 // because the only method for recovering from an immutability error is to 24 // delete the object and re-create it -- if the next apply will not succeed due 25 // to invalid fields, the immutability error cannot be recovered from. 26 // 27 // Error evaluation logic: 28 // 29 // 1. Nil errors and [k8s.io/apimachinery/pkg/api/errors.NewNotFound] return 30 // false, because an error can't be due to immutability if doesn't already 31 // exist. 32 // 2. [k8s.io/apimachinery/pkg/api/errors.NewConflict] errors return true. 33 // 3. If the error is [k8s.io/apimachinery/pkg/api/errors.NewInvalid], it is 34 // further probed for the specific cause, as immutability is just a subset 35 // of the potential invalid errors. Immutability errors should almost always 36 // indicate that the cause for the error is FieldValueForbidden, but some 37 // older buitl-in values are inconsistent and incidate the cause is 38 // FieldValueInvalid. If only FieldValueForbidden or FieldValueInvalid 39 // causes are detected, we return true. 40 // 4. The error message is evaluated for known immutability message regexes, 41 // e.g. CEL or custom admission webhooks. 42 func IsImmutableError(err error) bool { 43 if err == nil || kerrors.IsNotFound(err) { 44 return false 45 } 46 47 // Detect immutability like kubectl does, but try to be more specific about 48 // invalid errors to reduce false positives. 49 // https://github.com/kubernetes/kubectl/blob/8165f83007/pkg/cmd/apply/patcher.go#L201 50 if kerrors.IsConflict(err) { 51 return true 52 } 53 54 if kerrors.IsInvalid(err) { 55 // Attempt to ignore invalid errors caused by other issues (eg, simply an 56 // invalid object) by narrowing down to errors which are _only_ caused by 57 // forbidden and invalid field errors. 58 var status kerrors.APIStatus 59 // All Invalid errors must be APIErrors 60 errors.As(err, &status) 61 62 // If we encounter any causes that are not associated with immutability, 63 // return false. That is because the only way to handle immutability errors 64 // are deleting and re-creating. Detecting the presence of any error causes 65 // that would block the re-creation of the object ensures immutability 66 // errors are only handled when objects can cleanly be re-created. 67 // 68 // Value is hardcoded because missing constants were added in 1.28+: 69 // https://github.com/kubernetes/kubernetes/commit/79c02ceb73f4d64f52a9d4c46785e4c7497493d9#diff-6f084d57227fbb44adce838b4eed2e40680cef52677adce543dddf5edc1cd3c9R1002 70 for _, c := range status.Status().Details.Causes { 71 switch c.Type { 72 case metav1.CauseType("FieldValueForbidden"), metav1.CauseTypeFieldValueInvalid: 73 default: 74 return false 75 } 76 } 77 return true 78 } 79 80 // Detect immutable errors returned by custom admission webhooks and Kubernetes CEL 81 // https://kubernetes.io/blog/2022/09/29/enforce-immutability-using-cel/#immutablility-after-first-modification 82 for _, fieldError := range matchImmutableFieldErrors { 83 if fieldError.MatchString(err.Error()) { 84 return true 85 } 86 } 87 88 return false 89 } 90