mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 06:54:01 +00:00
namespace: Provide a special status cause when a namespace is terminating
Clients should be able to identify when a namespace is being terminated and take special action such as backing off or giving up. Add a helper for getting the cause of an error and then add a special cause to the forbidden error that namespace lifecycle admission returns. We can't change the forbidden reason without potentially breaking older clients and so cause is the appropriate tool. Add `StatusCause` and `HasStatusCause` to the errors package to make checking for causes simpler. Add `NamespaceTerminatingCause` to the v1 API as a constant.
This commit is contained in:
parent
ffc2227e3e
commit
a62c5b282f
@ -4659,6 +4659,12 @@ const (
|
||||
NamespaceTerminating NamespacePhase = "Terminating"
|
||||
)
|
||||
|
||||
const (
|
||||
// NamespaceTerminatingCause is returned as a defaults.cause item when a change is
|
||||
// forbidden due to the namespace being terminated.
|
||||
NamespaceTerminatingCause metav1.CauseType = "NamespaceTerminating"
|
||||
)
|
||||
|
||||
type NamespaceConditionType string
|
||||
|
||||
// These are valid conditions of a namespace.
|
||||
|
@ -70,6 +70,28 @@ func (e *StatusError) DebugError() (string, []interface{}) {
|
||||
return "server response object: %#v", []interface{}{e.ErrStatus}
|
||||
}
|
||||
|
||||
// HasStatusCause returns true if the provided error has a details cause
|
||||
// with the provided type name.
|
||||
func HasStatusCause(err error, name metav1.CauseType) bool {
|
||||
_, ok := StatusCause(err, name)
|
||||
return ok
|
||||
}
|
||||
|
||||
// StatusCause returns the named cause from the provided error if it exists and
|
||||
// the error is of the type APIStatus. Otherwise it returns false.
|
||||
func StatusCause(err error, name metav1.CauseType) (metav1.StatusCause, bool) {
|
||||
apierr, ok := err.(APIStatus)
|
||||
if !ok || apierr == nil || apierr.Status().Details == nil {
|
||||
return metav1.StatusCause{}, false
|
||||
}
|
||||
for _, cause := range apierr.Status().Details.Causes {
|
||||
if cause.Type == name {
|
||||
return cause, true
|
||||
}
|
||||
}
|
||||
return metav1.StatusCause{}, false
|
||||
}
|
||||
|
||||
// UnexpectedObjectError can be returned by FromObject if it's passed a non-status object.
|
||||
type UnexpectedObjectError struct {
|
||||
Object runtime.Object
|
||||
|
@ -34,10 +34,12 @@ go_test(
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/clock:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
|
@ -170,8 +170,15 @@ func (l *Lifecycle) Admit(ctx context.Context, a admission.Attributes, o admissi
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: This should probably not be a 403
|
||||
return admission.NewForbidden(a, fmt.Errorf("unable to create new content in namespace %s because it is being terminated", a.GetNamespace()))
|
||||
err := admission.NewForbidden(a, fmt.Errorf("unable to create new content in namespace %s because it is being terminated", a.GetNamespace()))
|
||||
if apierr, ok := err.(*errors.StatusError); ok {
|
||||
apierr.ErrStatus.Details.Causes = append(apierr.ErrStatus.Details.Causes, metav1.StatusCause{
|
||||
Type: v1.NamespaceTerminatingCause,
|
||||
Message: fmt.Sprintf("namespace %s is being terminated", a.GetNamespace()),
|
||||
Field: "metadata.namespace",
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -19,14 +19,17 @@ package lifecycle
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
@ -192,6 +195,14 @@ func TestAdmissionNamespaceTerminating(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error rejecting creates in a namespace when it is terminating")
|
||||
}
|
||||
expectedCause := metav1.StatusCause{
|
||||
Type: v1.NamespaceTerminatingCause,
|
||||
Message: fmt.Sprintf("namespace %s is being terminated", namespace),
|
||||
Field: "metadata.namespace",
|
||||
}
|
||||
if cause, ok := errors.StatusCause(err, v1.NamespaceTerminatingCause); !ok || !reflect.DeepEqual(expectedCause, cause) {
|
||||
t.Errorf("Expected status cause indicating the namespace is terminating: %t %s", ok, diff.ObjectReflectDiff(expectedCause, cause))
|
||||
}
|
||||
|
||||
// verify update operations in the namespace can proceed
|
||||
err = handler.Admit(context.TODO(), admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
|
||||
|
Loading…
Reference in New Issue
Block a user