Skip to content

Commit

Permalink
Adapt webhook/admission custom Validator and CustomValidator interfac…
Browse files Browse the repository at this point in the history
  • Loading branch information
ary1992 committed Jun 23, 2023
1 parent a166665 commit 5fffead
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 174 deletions.
Expand Up @@ -25,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

gardencore "github.com/gardener/gardener/pkg/apis/core"
gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
Expand All @@ -40,53 +41,53 @@ type Handler struct {
}

// ValidateCreate performs the check.
func (h *Handler) ValidateCreate(ctx context.Context, obj runtime.Object) error {
func (h *Handler) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()

secret, ok := obj.(*corev1.Secret)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected *corev1.Secret but got %T", obj))
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected *corev1.Secret but got %T", obj))
}

seedName := gardenerutils.ComputeSeedName(secret.Namespace)
if secret.Namespace != v1beta1constants.GardenNamespace && seedName == "" {
return nil
return nil, nil
}

exists, err := h.internalDomainSecretExists(ctx, secret.Namespace)
if err != nil {
return apierrors.NewInternalError(err)
return nil, apierrors.NewInternalError(err)
}
if exists {
return apierrors.NewConflict(schema.GroupResource{Group: corev1.GroupName, Resource: "Secret"}, secret.Name, fmt.Errorf("cannot create internal domain secret because there can be only one secret with the 'internal-domain' secret role per namespace"))
return nil, apierrors.NewConflict(schema.GroupResource{Group: corev1.GroupName, Resource: "Secret"}, secret.Name, fmt.Errorf("cannot create internal domain secret because there can be only one secret with the 'internal-domain' secret role per namespace"))
}

if _, _, _, _, _, err := gardenerutils.GetDomainInfoFromAnnotations(secret.Annotations); err != nil {
return apierrors.NewBadRequest(err.Error())
return nil, apierrors.NewBadRequest(err.Error())
}

return nil
return nil, nil
}

// ValidateUpdate performs the check.
func (h *Handler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
func (h *Handler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()

secret, ok := newObj.(*corev1.Secret)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected *corev1.Secret but got %T", newObj))
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected *corev1.Secret but got %T", newObj))
}

oldSecret, ok := oldObj.(*corev1.Secret)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected *corev1.Secret but got %T", oldObj))
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected *corev1.Secret but got %T", oldObj))
}

seedName := gardenerutils.ComputeSeedName(secret.Namespace)
if secret.Namespace != v1beta1constants.GardenNamespace && seedName == "" {
return nil
return nil, nil
}

// If secret was newly labeled with gardener.cloud/role=internal-domain then check whether another internal domain
Expand All @@ -95,59 +96,59 @@ func (h *Handler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Obj
secret.Labels[v1beta1constants.GardenRole] == v1beta1constants.GardenRoleInternalDomain {
exists, err := h.internalDomainSecretExists(ctx, secret.Namespace)
if err != nil {
return apierrors.NewInternalError(err)
return nil, apierrors.NewInternalError(err)
}
if exists {
return apierrors.NewConflict(schema.GroupResource{Group: corev1.GroupName, Resource: "Secret"}, secret.Name, fmt.Errorf("cannot update secret because there can be only one secret with the 'internal-domain' secret role per namespace"))
return nil, apierrors.NewConflict(schema.GroupResource{Group: corev1.GroupName, Resource: "Secret"}, secret.Name, fmt.Errorf("cannot update secret because there can be only one secret with the 'internal-domain' secret role per namespace"))
}
}

_, oldDomain, _, _, _, err := gardenerutils.GetDomainInfoFromAnnotations(oldSecret.Annotations)
if err != nil {
return apierrors.NewInternalError(err)
return nil, apierrors.NewInternalError(err)
}
_, newDomain, _, _, _, err := gardenerutils.GetDomainInfoFromAnnotations(secret.Annotations)
if err != nil {
return apierrors.NewInternalError(err)
return nil, apierrors.NewInternalError(err)
}

if oldDomain != newDomain {
atLeastOneShoot, err := h.atLeastOneShootExists(ctx, seedName)
if err != nil {
return apierrors.NewInternalError(err)
return nil, apierrors.NewInternalError(err)
}
if atLeastOneShoot {
return apierrors.NewForbidden(schema.GroupResource{Group: corev1.GroupName, Resource: "Secret"}, secret.Name, fmt.Errorf("cannot change domain because there are still shoots left in the system"))
return nil, apierrors.NewForbidden(schema.GroupResource{Group: corev1.GroupName, Resource: "Secret"}, secret.Name, fmt.Errorf("cannot change domain because there are still shoots left in the system"))
}
}

return nil
return nil, nil
}

// ValidateDelete performs the check.
func (h *Handler) ValidateDelete(ctx context.Context, obj runtime.Object) error {
func (h *Handler) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()

secret, ok := obj.(*corev1.Secret)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected *corev1.Secret but got %T", obj))
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected *corev1.Secret but got %T", obj))
}

seedName := gardenerutils.ComputeSeedName(secret.Namespace)
if secret.Namespace != v1beta1constants.GardenNamespace && seedName == "" {
return nil
return nil, nil
}

atLeastOneShoot, err := h.atLeastOneShootExists(ctx, seedName)
if err != nil {
return apierrors.NewInternalError(err)
return nil, apierrors.NewInternalError(err)
}
if atLeastOneShoot {
return apierrors.NewForbidden(schema.GroupResource{Group: corev1.GroupName, Resource: "Secret"}, secret.Name, fmt.Errorf("cannot delete internal domain secret because there are still shoots left in the system"))
return nil, apierrors.NewForbidden(schema.GroupResource{Group: corev1.GroupName, Resource: "Secret"}, secret.Name, fmt.Errorf("cannot delete internal domain secret because there are still shoots left in the system"))
}

return nil
return nil, nil
}

func (h *Handler) atLeastOneShootExists(ctx context.Context, seedName string) (bool, error) {
Expand Down
Expand Up @@ -111,7 +111,9 @@ var _ = Describe("handler", func() {
client.Limit(1),
).Return(fakeErr)

err := handler.ValidateCreate(ctx, secret)
warnings, err := handler.ValidateCreate(ctx, secret)
Expect(warnings).To(BeNil())

statusError, ok := err.(*apierrors.StatusError)
Expect(ok).To(BeTrue())
Expect(statusError.Status().Code).To(Equal(int32(http.StatusInternalServerError)))
Expand All @@ -130,7 +132,9 @@ var _ = Describe("handler", func() {
return nil
})

Expect(handler.ValidateCreate(ctx, secret)).To(MatchError(ContainSubstring("there can be only one secret with the 'internal-domain' secret role")))
warnings, err := handler.ValidateCreate(ctx, secret)
Expect(warnings).To(BeNil())
Expect(err).To(MatchError(ContainSubstring("there can be only one secret with the 'internal-domain' secret role")))
})

It("should fail because another internal domain secret exists in the same seed namespace", func() {
Expand Down Expand Up @@ -193,7 +197,9 @@ var _ = Describe("handler", func() {
client.Limit(1),
).Return(fakeErr)

err := handler.ValidateUpdate(ctx, oldSecret, secret)
warnings, err := handler.ValidateUpdate(ctx, oldSecret, secret)
Expect(warnings).To(BeNil())

statusError, ok := err.(*apierrors.StatusError)
Expect(ok).To(BeTrue())
Expect(statusError.Status().Code).To(Equal(int32(http.StatusInternalServerError)))
Expand Down Expand Up @@ -238,7 +244,9 @@ var _ = Describe("handler", func() {
oldSecret := secret.DeepCopy()
secret.Annotations["dns.gardener.cloud/domain"] = "foobar"

err := handler.ValidateUpdate(ctx, oldSecret, secret)
warnings, err := handler.ValidateUpdate(ctx, oldSecret, secret)
Expect(warnings).To(BeNil())

statusError, ok := err.(*apierrors.StatusError)
Expect(ok).To(BeTrue())
Expect(statusError.Status().Code).To(Equal(int32(http.StatusInternalServerError)))
Expand Down Expand Up @@ -304,7 +312,9 @@ var _ = Describe("handler", func() {
client.Limit(1),
).Return(fakeErr)

err := handler.ValidateDelete(ctx, secret)
warnings, err := handler.ValidateDelete(ctx, secret)
Expect(warnings).To(BeNil())

statusError, ok := err.(*apierrors.StatusError)
Expect(ok).To(BeTrue())
Expect(statusError.Status().Code).To(Equal(int32(http.StatusInternalServerError)))
Expand Down
Expand Up @@ -41,46 +41,46 @@ type Handler struct {
}

// ValidateCreate performs the check.
func (h *Handler) ValidateCreate(ctx context.Context, obj runtime.Object) error {
func (h *Handler) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
return h.handle(ctx, obj)
}

// ValidateUpdate performs the check.
func (h *Handler) ValidateUpdate(ctx context.Context, _, newObj runtime.Object) error {
func (h *Handler) ValidateUpdate(ctx context.Context, _, newObj runtime.Object) (admission.Warnings, error) {
return h.handle(ctx, newObj)
}

// ValidateDelete returns nil (not implemented by this handler).
func (h *Handler) ValidateDelete(_ context.Context, _ runtime.Object) error {
return nil
func (h *Handler) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
return nil, nil
}

func (h *Handler) handle(ctx context.Context, obj runtime.Object) error {
func (h *Handler) handle(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
secret, ok := obj.(*corev1.Secret)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected *corev1.Secret but got %T", obj))
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected *corev1.Secret but got %T", obj))
}

req, err := admission.RequestFromContext(ctx)
if err != nil {
return apierrors.NewInternalError(err)
return nil, apierrors.NewInternalError(err)
}

kubeconfig, ok := secret.Data[kubernetes.KubeConfig]
if !ok {
return nil
return nil, nil
}

clientConfig, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
if err != nil {
return apierrors.NewBadRequest(err.Error())
return nil, apierrors.NewBadRequest(err.Error())
}

// Validate that the given kubeconfig doesn't have fields in its auth-info that are
// not acceptable.
rawConfig, err := clientConfig.RawConfig()
if err != nil {
return apierrors.NewBadRequest(err.Error())
return nil, apierrors.NewBadRequest(err.Error())
}

if err := kubernetes.ValidateConfig(rawConfig); err != nil {
Expand All @@ -98,8 +98,8 @@ func (h *Handler) handle(ctx context.Context, obj runtime.Object) error {
metricReasonRejectedKubeconfig,
).Inc()

return apierrors.NewInvalid(schema.GroupKind{Group: corev1.GroupName, Kind: "Secret"}, secret.Name, field.ErrorList{field.Invalid(field.NewPath("data", "kubeconfig"), kubeconfig, fmt.Sprintf("secret contains invalid kubeconfig: %s", err))})
return nil, apierrors.NewInvalid(schema.GroupKind{Group: corev1.GroupName, Kind: "Secret"}, secret.Name, field.ErrorList{field.Invalid(field.NewPath("data", "kubeconfig"), kubeconfig, fmt.Sprintf("secret contains invalid kubeconfig: %s", err))})
}

return nil
return nil, nil
}
Expand Up @@ -40,70 +40,70 @@ type Handler struct {
}

// ValidateCreate returns nil (not implemented by this handler).
func (h *Handler) ValidateCreate(_ context.Context, _ runtime.Object) error {
return nil
func (h *Handler) ValidateCreate(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
return nil, nil
}

// ValidateUpdate returns nil (not implemented by this handler).
func (h *Handler) ValidateUpdate(_ context.Context, _, _ runtime.Object) error {
return nil
func (h *Handler) ValidateUpdate(_ context.Context, _, _ runtime.Object) (admission.Warnings, error) {
return nil, nil
}

// ValidateDelete validates the namespace deletion.
func (h *Handler) ValidateDelete(ctx context.Context, _ runtime.Object) error {
func (h *Handler) ValidateDelete(ctx context.Context, _ runtime.Object) (admission.Warnings, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()

req, err := admission.RequestFromContext(ctx)
if err != nil {
return apierrors.NewInternalError(err)
return nil, apierrors.NewInternalError(err)
}

if err := h.admitNamespace(ctx, req.Name); err != nil {
h.Logger.Info("Rejected namespace deletion", "user", req.UserInfo.Username, "reason", err.Error())
return err
}

return nil
return nil, nil
}

// admitNamespace does only allow the request if no Shoots exist in this specific namespace anymore.
func (h *Handler) admitNamespace(ctx context.Context, namespaceName string) error {
func (h *Handler) admitNamespace(ctx context.Context, namespaceName string) (admission.Warnings, error) {
// Determine project for given namespace.
// TODO: we should use a direct lookup here, as we might falsely allow the request, if our cache is
// out of sync and doesn't know about the project. We should use a field selector for looking up the project
// belonging to a given namespace.
project, namespace, err := gardenerutils.ProjectAndNamespaceFromReader(ctx, h.Client, namespaceName)
if err != nil {
if apierrors.IsNotFound(err) {
return nil
return nil, nil
}
return apierrors.NewInternalError(err)
return nil, apierrors.NewInternalError(err)
}

if project == nil {
return nil
return nil, nil
}

switch {
case namespace.DeletionTimestamp != nil:
return nil
return nil, nil

case project.DeletionTimestamp != nil:
// if project is marked for deletion we need to wait until all shoots in the namespace are gone
namespaceInUse, err := kubernetesutils.ResourcesExist(ctx, h.APIReader, gardencorev1beta1.SchemeGroupVersion.WithKind("ShootList"), client.InNamespace(namespace.Name))
if err != nil {
return apierrors.NewInternalError(err)
return nil, apierrors.NewInternalError(err)
}

if !namespaceInUse {
return nil
return nil, nil
}

return apierrors.NewForbidden(schema.GroupResource{Group: corev1.GroupName, Resource: "Namespace"}, namespace.Name, fmt.Errorf("deletion of namespace %q is not permitted (it still contains Shoots)", namespace.Name))
return nil, apierrors.NewForbidden(schema.GroupResource{Group: corev1.GroupName, Resource: "Namespace"}, namespace.Name, fmt.Errorf("deletion of namespace %q is not permitted (it still contains Shoots)", namespace.Name))
}

// Namespace is not yet marked for deletion and project is not marked as well. We do not admit and respond that
// namespace deletion is only allowed via project deletion.
return apierrors.NewForbidden(schema.GroupResource{Group: corev1.GroupName, Resource: "Namespace"}, namespace.Name, fmt.Errorf("direct deletion of namespace %q is not permitted (you must delete the corresponding project %q)", namespace.Name, project.Name))
return nil, apierrors.NewForbidden(schema.GroupResource{Group: corev1.GroupName, Resource: "Namespace"}, namespace.Name, fmt.Errorf("direct deletion of namespace %q is not permitted (you must delete the corresponding project %q)", namespace.Name, project.Name))
}

0 comments on commit 5fffead

Please sign in to comment.