// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0

package e2e

import (
	"context"
	"fmt"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
	"github.com/projectcapsule/capsule/pkg/api/meta"
	"github.com/projectcapsule/capsule/pkg/api/rbac"
)

var _ = Describe("preventing PersistentVolume cross-tenant mount", Ordered, Label("tenant", "storage", "persistentvolumeclaim"), func() {
	tnt1 := &capsulev1beta2.Tenant{
		ObjectMeta: metav1.ObjectMeta{
			Name: "e2e-pv-cross-one",
			Labels: map[string]string{
				"env": "e2e",
			},
		},
		Spec: capsulev1beta2.TenantSpec{
			Owners: rbac.OwnerListSpec{
				{
					CoreOwnerSpec: rbac.CoreOwnerSpec{
						UserSpec: rbac.UserSpec{
							Name: "e2e-pv-cross-one",
							Kind: "User",
						},
					},
				},
			},
		},
	}

	tnt2 := &capsulev1beta2.Tenant{
		ObjectMeta: metav1.ObjectMeta{
			Name: "e2e-pv-cross-two",
			Labels: map[string]string{
				"env": "e2e",
			},
		},
		Spec: capsulev1beta2.TenantSpec{
			Owners: rbac.OwnerListSpec{
				{
					CoreOwnerSpec: rbac.CoreOwnerSpec{
						UserSpec: rbac.UserSpec{
							Name: "e2e-pv-cross-two",
							Kind: "User",
						},
					},
				},
			},
		},
	}

	JustBeforeEach(func() {
		for _, tnt := range []*capsulev1beta2.Tenant{tnt1, tnt2} {
			EventuallyCreation(func() error {
				tnt.ResourceVersion = ""

				return k8sClient.Create(context.TODO(), tnt)
			}).Should(Succeed())

			TenantReady(tnt, metav1.ConditionTrue, defaultTimeoutInterval)
		}
	})

	JustAfterEach(func() {
		for _, tnt := range []*capsulev1beta2.Tenant{tnt1, tnt2} {
			EventuallyDeletion(tnt)
		}
	})

	It("should add labels to PersistentVolume and prevent cross-Tenant mount", Label("skip-on-openshift"), func() {
		ns := NewNamespace("", map[string]string{
			meta.TenantLabel: tnt1.GetName(),
		})
		NamespaceCreation(ns, tnt1.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
		NamespaceIsPartOfTenant(tnt1, ns).Should(Succeed())

		pvc := corev1.PersistentVolumeClaim{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "arrakis",
				Namespace: ns.Name,
			},
			Spec: corev1.PersistentVolumeClaimSpec{
				AccessModes: []corev1.PersistentVolumeAccessMode{
					corev1.ReadWriteOnce,
				},
				Resources: corev1.VolumeResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceStorage: resource.MustParse("1Gi"),
					},
				},
				StorageClassName: ptr.To("standard"),
			},
		}

		EventuallyCreation(func() error {
			return k8sClient.Create(context.Background(), &pvc)
		}).Should(Succeed())

		pod := corev1.Pod{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "arrakis-pod",
				Namespace: ns.Name,
			},
			Spec: corev1.PodSpec{
				SecurityContext: nobodyPodSecurityContext(),
				Containers: []corev1.Container{
					{
						Name:            "container",
						Image:           "registry.k8s.io/pause:3.10",
						ImagePullPolicy: corev1.PullAlways,
						SecurityContext: restrictedContainerSecurityContext(),
						VolumeMounts: []corev1.VolumeMount{
							{
								Name:      "data",
								MountPath: "/tmp",
							},
						},
					},
				},
				Volumes: []corev1.Volume{
					{
						Name: "data",
						VolumeSource: corev1.VolumeSource{
							PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
								ClaimName: pvc.Name,
							},
						},
					},
				},
			},
		}

		EventuallyCreation(func() error {
			return k8sClient.Create(context.Background(), &pod)
		}).Should(Succeed())

		Eventually(func() int {
			nsName := types.NamespacedName{Name: pvc.Name, Namespace: pvc.Namespace}

			if err := k8sClient.Get(context.Background(), nsName, &pvc); err != nil {
				return 0
			}

			return len(pvc.Spec.VolumeName)
		}, defaultTimeoutInterval, defaultPollInterval).Should(BeNumerically(">", 0))

		pv := corev1.PersistentVolume{}
		defer func() {
			_ = k8sClient.Delete(context.Background(), &pv)
		}()

		Eventually(func() string {
			if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: pvc.Spec.VolumeName}, &pv); err != nil {
				return "not-found"
			}

			if pv.GetLabels() == nil {
				return "no-labels"
			}

			return pv.GetLabels()["capsule.clastix.io/tenant"]
		}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(tnt1.Name))

		Eventually(func() error {
			nsName := types.NamespacedName{Name: pv.Name}

			if err := k8sClient.Get(context.Background(), nsName, &pv); err != nil {
				return err
			}

			pv.Spec.PersistentVolumeReclaimPolicy = corev1.PersistentVolumeReclaimRecycle

			return k8sClient.Update(context.Background(), &pv)
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())

		Expect(k8sClient.Delete(context.Background(), &pod, &client.DeleteOptions{GracePeriodSeconds: ptr.To(int64(0))})).ToNot(HaveOccurred())

		ns2 := NewNamespace("", map[string]string{
			meta.TenantLabel: tnt2.GetName(),
		})
		NamespaceCreation(ns2, tnt2.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
		NamespaceIsPartOfTenant(tnt2, ns2).Should(Succeed())

		Consistently(func() error {
			pvc := corev1.PersistentVolumeClaim{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "caladan",
					Namespace: ns2.Name,
				},
				Spec: corev1.PersistentVolumeClaimSpec{
					AccessModes: []corev1.PersistentVolumeAccessMode{
						corev1.ReadWriteOnce,
					},
					Resources: corev1.VolumeResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceStorage: resource.MustParse("1Gi"),
						},
					},
					StorageClassName: ptr.To("standard"),
					VolumeName:       pv.Name,
				},
			}

			return k8sClient.Create(context.Background(), &pvc)
		}, defaultTimeoutInterval, defaultPollInterval).Should(HaveOccurred())
	})

	It("should repair empty and stale Tenant labels on claimed PersistentVolumes", Label("skip-on-openshift"), func() {
		ns := NewNamespace("", map[string]string{
			meta.TenantLabel: tnt1.GetName(),
		})
		NamespaceCreation(ns, tnt1.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())

		volumes := make([]*corev1.PersistentVolume, 0, 2)
		defer func() {
			for _, pv := range volumes {
				_ = k8sClient.Delete(context.Background(), pv)
			}
		}()

		for index, value := range []string{"", tnt2.Name} {
			pv := &corev1.PersistentVolume{
				ObjectMeta: metav1.ObjectMeta{
					Name: fmt.Sprintf("repair-tenant-label-%d", index),
					Labels: map[string]string{
						meta.TenantLabel: value,
					},
				},
				Spec: corev1.PersistentVolumeSpec{
					Capacity: corev1.ResourceList{
						corev1.ResourceStorage: resource.MustParse("1Gi"),
					},
					AccessModes: []corev1.PersistentVolumeAccessMode{
						corev1.ReadWriteOnce,
					},
					PersistentVolumeReclaimPolicy: corev1.PersistentVolumeReclaimRetain,
					StorageClassName:              "manual",
					ClaimRef: &corev1.ObjectReference{
						APIVersion: "v1",
						Kind:       "PersistentVolumeClaim",
						Namespace:  ns.Name,
						Name:       fmt.Sprintf("repair-tenant-label-%d", index),
					},
					PersistentVolumeSource: corev1.PersistentVolumeSource{
						HostPath: &corev1.HostPathVolumeSource{
							Path: fmt.Sprintf("/tmp/capsule-e2e-repair-tenant-label-%d", index),
						},
					},
				},
			}
			volumes = append(volumes, pv)

			EventuallyCreation(func() error {
				return k8sClient.Create(context.Background(), pv)
			}).Should(Succeed())

			Eventually(func(g Gomega) {
				current := &corev1.PersistentVolume{}
				g.Expect(k8sClient.Get(
					context.Background(),
					types.NamespacedName{Name: pv.Name},
					current,
				)).To(Succeed())
				g.Expect(current.GetLabels()).To(HaveKeyWithValue(meta.TenantLabel, tnt1.Name))
			}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
		}
	})

	It("should not add a selector when updating an already-bound dynamic PVC without selector", func() {
		ns := NewNamespace("", map[string]string{
			meta.TenantLabel: tnt1.GetName(),
		})
		NamespaceCreation(ns, tnt1.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
		NamespaceIsPartOfTenant(tnt1, ns).Should(Succeed())

		pvc := corev1.PersistentVolumeClaim{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "dynamic-pvc",
				Namespace: ns.Name,
			},
			Spec: corev1.PersistentVolumeClaimSpec{
				AccessModes: []corev1.PersistentVolumeAccessMode{
					corev1.ReadWriteOnce,
				},
				Resources: corev1.VolumeResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceStorage: resource.MustParse("1Gi"),
					},
				},
				StorageClassName: ptr.To("standard"),
			},
		}

		EventuallyCreation(func() error {
			return k8sClient.Create(context.Background(), &pvc)
		}).Should(Succeed())

		Eventually(func(g Gomega) {
			g.Expect(k8sClient.Get(
				context.Background(),
				types.NamespacedName{Name: pvc.Name, Namespace: pvc.Namespace},
				&pvc,
			)).To(Succeed())

			g.Expect(pvc.Spec.Selector).To(BeNil())
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())

		pvc.Labels = map[string]string{
			"updated": "true",
		}

		Eventually(func() error {
			return k8sClient.Update(context.Background(), &pvc)
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())

		Eventually(func(g Gomega) {
			updated := corev1.PersistentVolumeClaim{}

			g.Expect(k8sClient.Get(
				context.Background(),
				types.NamespacedName{Name: pvc.Name, Namespace: pvc.Namespace},
				&updated,
			)).To(Succeed())

			g.Expect(updated.Labels).To(HaveKeyWithValue("updated", "true"))
			g.Expect(updated.Spec.Selector).To(BeNil())
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
	})

	It("should add the Tenant selector to a PVC with an existing selector", func() {
		ns := NewNamespace("", map[string]string{
			meta.TenantLabel: tnt1.GetName(),
		})
		NamespaceCreation(ns, tnt1.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
		NamespaceIsPartOfTenant(tnt1, ns).Should(Succeed())

		pvc := corev1.PersistentVolumeClaim{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "selector-pvc",
				Namespace: ns.Name,
			},
			Spec: corev1.PersistentVolumeClaimSpec{
				AccessModes: []corev1.PersistentVolumeAccessMode{
					corev1.ReadWriteOnce,
				},
				Resources: corev1.VolumeResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceStorage: resource.MustParse("1Gi"),
					},
				},
				StorageClassName: ptr.To("standard"),
				Selector: &metav1.LabelSelector{
					MatchLabels: map[string]string{
						"storage-tier": "gold",
					},
				},
			},
		}

		EventuallyCreation(func() error {
			return k8sClient.Create(context.Background(), &pvc)
		}).Should(Succeed())

		Eventually(func(g Gomega) {
			created := corev1.PersistentVolumeClaim{}

			g.Expect(k8sClient.Get(
				context.Background(),
				types.NamespacedName{Name: pvc.Name, Namespace: pvc.Namespace},
				&created,
			)).To(Succeed())

			g.Expect(created.Spec.Selector).ToNot(BeNil())
			g.Expect(created.Spec.Selector.MatchLabels).To(HaveKeyWithValue("storage-tier", "gold"))
			g.Expect(created.Spec.Selector.MatchLabels).ToNot(HaveKey(meta.TenantLabel))
			g.Expect(created.Spec.Selector.MatchExpressions).To(ContainElement(metav1.LabelSelectorRequirement{
				Key:      meta.TenantLabel,
				Operator: metav1.LabelSelectorOpIn,
				Values:   []string{tnt1.Name},
			}))
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
	})

	It("should overwrite conflicting Tenant selectors on PVC creation", func() {
		ns := NewNamespace("", map[string]string{
			meta.TenantLabel: tnt1.GetName(),
		})
		NamespaceCreation(ns, tnt1.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
		NamespaceIsPartOfTenant(tnt1, ns).Should(Succeed())

		pvc := corev1.PersistentVolumeClaim{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "conflicting-selector-pvc",
				Namespace: ns.Name,
			},
			Spec: corev1.PersistentVolumeClaimSpec{
				AccessModes: []corev1.PersistentVolumeAccessMode{
					corev1.ReadWriteOnce,
				},
				Resources: corev1.VolumeResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceStorage: resource.MustParse("1Gi"),
					},
				},
				StorageClassName: ptr.To("standard"),
				Selector: &metav1.LabelSelector{
					MatchLabels: map[string]string{
						meta.TenantLabel: tnt2.Name,
						"storage-tier":   "gold",
					},
					MatchExpressions: []metav1.LabelSelectorRequirement{
						{
							Key:      meta.TenantLabel,
							Operator: metav1.LabelSelectorOpNotIn,
							Values:   []string{tnt1.Name},
						},
						{
							Key:      "environment",
							Operator: metav1.LabelSelectorOpIn,
							Values:   []string{"test"},
						},
					},
				},
			},
		}

		EventuallyCreation(func() error {
			return k8sClient.Create(context.Background(), &pvc)
		}).Should(Succeed())

		Eventually(func(g Gomega) {
			created := corev1.PersistentVolumeClaim{}

			g.Expect(k8sClient.Get(
				context.Background(),
				types.NamespacedName{Name: pvc.Name, Namespace: pvc.Namespace},
				&created,
			)).To(Succeed())

			g.Expect(created.Spec.Selector).ToNot(BeNil())

			// The mutating webhook must preserve unrelated selector requirements.
			g.Expect(created.Spec.Selector.MatchLabels).To(HaveKeyWithValue("storage-tier", "gold"))
			g.Expect(created.Spec.Selector.MatchExpressions).To(ContainElement(metav1.LabelSelectorRequirement{
				Key:      "environment",
				Operator: metav1.LabelSelectorOpIn,
				Values:   []string{"test"},
			}))

			// The tenant selector must be canonicalized.
			g.Expect(created.Spec.Selector.MatchLabels).ToNot(HaveKey(meta.TenantLabel))

			tenantExpressions := 0
			for _, expression := range created.Spec.Selector.MatchExpressions {
				if expression.Key != meta.TenantLabel {
					continue
				}

				tenantExpressions++

				g.Expect(expression.Operator).To(Equal(metav1.LabelSelectorOpIn))
				g.Expect(expression.Values).To(Equal([]string{tnt1.Name}))
			}

			g.Expect(tenantExpressions).To(Equal(1))
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
	})

	It("should add the Tenant selector to a pre-bound PVC", func() {
		ns := NewNamespace("", map[string]string{
			meta.TenantLabel: tnt1.GetName(),
		})
		NamespaceCreation(ns, tnt1.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
		NamespaceIsPartOfTenant(tnt1, ns).Should(Succeed())

		pv := corev1.PersistentVolume{
			ObjectMeta: metav1.ObjectMeta{
				Name: "prebound-pv",
				Labels: map[string]string{
					meta.TenantLabel: tnt1.Name,
				},
			},
			Spec: corev1.PersistentVolumeSpec{
				Capacity: corev1.ResourceList{
					corev1.ResourceStorage: resource.MustParse("1Gi"),
				},
				AccessModes: []corev1.PersistentVolumeAccessMode{
					corev1.ReadWriteOnce,
				},
				PersistentVolumeReclaimPolicy: corev1.PersistentVolumeReclaimRetain,
				StorageClassName:              "manual",
				PersistentVolumeSource: corev1.PersistentVolumeSource{
					HostPath: &corev1.HostPathVolumeSource{
						Path: "/tmp/capsule-e2e-prebound-pv",
					},
				},
			},
		}

		EventuallyCreation(func() error {
			return k8sClient.Create(context.Background(), &pv)
		}).Should(Succeed())

		defer func() {
			_ = k8sClient.Delete(context.Background(), &pv)
		}()

		pvc := corev1.PersistentVolumeClaim{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "prebound-pvc",
				Namespace: ns.Name,
			},
			Spec: corev1.PersistentVolumeClaimSpec{
				AccessModes: []corev1.PersistentVolumeAccessMode{
					corev1.ReadWriteOnce,
				},
				Resources: corev1.VolumeResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceStorage: resource.MustParse("1Gi"),
					},
				},
				StorageClassName: ptr.To("manual"),
				VolumeName:       pv.Name,
			},
		}

		EventuallyCreation(func() error {
			return k8sClient.Create(context.Background(), &pvc)
		}).Should(Succeed())

		Eventually(func(g Gomega) {
			created := corev1.PersistentVolumeClaim{}

			g.Expect(k8sClient.Get(
				context.Background(),
				types.NamespacedName{Name: pvc.Name, Namespace: pvc.Namespace},
				&created,
			)).To(Succeed())

			g.Expect(created.Spec.VolumeName).To(Equal(pv.Name))
			g.Expect(created.Spec.Selector).ToNot(BeNil())
			g.Expect(created.Spec.Selector.MatchExpressions).To(ContainElement(metav1.LabelSelectorRequirement{
				Key:      meta.TenantLabel,
				Operator: metav1.LabelSelectorOpIn,
				Values:   []string{tnt1.Name},
			}))
		}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
	})

})
