package runtime import ( "errors" "regexp" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Match CEL and known custom webhook immutable error variants. var matchImmutableFieldErrors = []*regexp.Regexp{ regexp.MustCompile(`.*is\simmutable.*`), regexp.MustCompile(`.*immutable\sfield.*`), // KCC famous deadhooks } // IsImmutableError checks if the given error indicates that a recoverable // immutability error has been encountered, which occurs when an object failed // to be applied strictly due to updating immutable fields. If there are // additional causes detected (e.g., the error is caused both by an immutable // field being changed and another field being invalid), the immutability error // is considered non-recoverable and this function returns false. This is // because the only method for recovering from an immutability error is to // delete the object and re-create it -- if the next apply will not succeed due // to invalid fields, the immutability error cannot be recovered from. // // Error evaluation logic: // // 1. Nil errors and [k8s.io/apimachinery/pkg/api/errors.NewNotFound] return // false, because an error can't be due to immutability if doesn't already // exist. // 2. [k8s.io/apimachinery/pkg/api/errors.NewConflict] errors return true. // 3. If the error is [k8s.io/apimachinery/pkg/api/errors.NewInvalid], it is // further probed for the specific cause, as immutability is just a subset // of the potential invalid errors. Immutability errors should almost always // indicate that the cause for the error is FieldValueForbidden, but some // older buitl-in values are inconsistent and incidate the cause is // FieldValueInvalid. If only FieldValueForbidden or FieldValueInvalid // causes are detected, we return true. // 4. The error message is evaluated for known immutability message regexes, // e.g. CEL or custom admission webhooks. func IsImmutableError(err error) bool { if err == nil || kerrors.IsNotFound(err) { return false } // Detect immutability like kubectl does, but try to be more specific about // invalid errors to reduce false positives. // https://github.com/kubernetes/kubectl/blob/8165f83007/pkg/cmd/apply/patcher.go#L201 if kerrors.IsConflict(err) { return true } if kerrors.IsInvalid(err) { // Attempt to ignore invalid errors caused by other issues (eg, simply an // invalid object) by narrowing down to errors which are _only_ caused by // forbidden and invalid field errors. var status kerrors.APIStatus // All Invalid errors must be APIErrors errors.As(err, &status) // If we encounter any causes that are not associated with immutability, // return false. That is because the only way to handle immutability errors // are deleting and re-creating. Detecting the presence of any error causes // that would block the re-creation of the object ensures immutability // errors are only handled when objects can cleanly be re-created. // // Value is hardcoded because missing constants were added in 1.28+: // https://github.com/kubernetes/kubernetes/commit/79c02ceb73f4d64f52a9d4c46785e4c7497493d9#diff-6f084d57227fbb44adce838b4eed2e40680cef52677adce543dddf5edc1cd3c9R1002 for _, c := range status.Status().Details.Causes { switch c.Type { case metav1.CauseType("FieldValueForbidden"), metav1.CauseTypeFieldValueInvalid: default: return false } } return true } // Detect immutable errors returned by custom admission webhooks and Kubernetes CEL // https://kubernetes.io/blog/2022/09/29/enforce-immutability-using-cel/#immutablility-after-first-modification for _, fieldError := range matchImmutableFieldErrors { if fieldError.MatchString(err.Error()) { return true } } return false }