|
| 1 | +package lineage |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "testing" |
| 6 | + |
| 7 | + helmv2 "github.com/fluxcd/helm-controller/api/v2" |
| 8 | + "k8s.io/apimachinery/pkg/api/meta" |
| 9 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 10 | + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
| 11 | + "k8s.io/apimachinery/pkg/runtime" |
| 12 | + "k8s.io/apimachinery/pkg/runtime/schema" |
| 13 | + dynamicfake "k8s.io/client-go/dynamic/fake" |
| 14 | +) |
| 15 | + |
| 16 | +const capkNamespace = "tenant-demo" |
| 17 | + |
| 18 | +// kubernetesAppMapper maps the cluster's HelmRelease to its Kubernetes |
| 19 | +// application, as the webhook's ApplicationDefinition mapper does. |
| 20 | +type kubernetesAppMapper struct{} |
| 21 | + |
| 22 | +func (kubernetesAppMapper) Map(*helmv2.HelmRelease) (string, string, string, error) { |
| 23 | + return "apps.cozystack.io/v1alpha1", "Kubernetes", "kubernetes-", nil |
| 24 | +} |
| 25 | + |
| 26 | +func capkObject(apiVersion, kind, name string, labels map[string]string, owner *unstructured.Unstructured) *unstructured.Unstructured { |
| 27 | + o := &unstructured.Unstructured{} |
| 28 | + o.SetAPIVersion(apiVersion) |
| 29 | + o.SetKind(kind) |
| 30 | + o.SetNamespace(capkNamespace) |
| 31 | + o.SetName(name) |
| 32 | + o.SetLabels(labels) |
| 33 | + if owner != nil { |
| 34 | + o.SetOwnerReferences([]metav1.OwnerReference{{APIVersion: owner.GetAPIVersion(), Kind: owner.GetKind(), Name: owner.GetName()}}) |
| 35 | + } |
| 36 | + return o |
| 37 | +} |
| 38 | + |
| 39 | +// capkNodeChain builds the objects behind the virt-launcher pod of a |
| 40 | +// Kubernetes worker node, as observed on a live cluster: the pod is owned by |
| 41 | +// its VMI and the VMI by its VM, but CAPK leaves the VM without an |
| 42 | +// ownerReference and points at its KubevirtMachine through labels only. From |
| 43 | +// the KubevirtMachine up, ownerReferences run to the CAPI Cluster, which the |
| 44 | +// cluster's HelmRelease labels. |
| 45 | +func capkNodeChain(vmLabels map[string]string) (*unstructured.Unstructured, []runtime.Object, meta.RESTMapper) { |
| 46 | + app := capkObject("apps.cozystack.io/v1alpha1", "Kubernetes", "cluster-a", nil, nil) |
| 47 | + hr := capkObject(HRAPIVersion, HRKind, "kubernetes-cluster-a", nil, nil) |
| 48 | + cluster := capkObject("cluster.x-k8s.io/v1beta1", "Cluster", "kubernetes-cluster-a", map[string]string{HRLabel: "kubernetes-cluster-a"}, nil) |
| 49 | + md := capkObject("cluster.x-k8s.io/v1beta1", "MachineDeployment", "kubernetes-cluster-a-md0", nil, cluster) |
| 50 | + ms := capkObject("cluster.x-k8s.io/v1beta1", "MachineSet", "kubernetes-cluster-a-md0-x", nil, md) |
| 51 | + machine := capkObject("cluster.x-k8s.io/v1beta1", "Machine", "kubernetes-cluster-a-md0-x-y", nil, ms) |
| 52 | + kvm := capkObject(KubevirtMachineAPIVersion, KubevirtMachineKind, "kubernetes-cluster-a-md0-x-y", nil, machine) |
| 53 | + vm := capkObject(VMAPIVersion, VMKind, "kubernetes-cluster-a-md0-x-y", vmLabels, nil) |
| 54 | + vmi := capkObject("kubevirt.io/v1", "VirtualMachineInstance", "kubernetes-cluster-a-md0-x-y", nil, vm) |
| 55 | + pod := capkObject("v1", "Pod", "virt-launcher-kubernetes-cluster-a-md0-x-y-abcde", nil, vmi) |
| 56 | + |
| 57 | + objs := []runtime.Object{app, hr, cluster, md, ms, machine, kvm, vm, vmi} |
| 58 | + mapper := meta.NewDefaultRESTMapper(nil) |
| 59 | + for _, o := range objs { |
| 60 | + mapper.Add(o.GetObjectKind().GroupVersionKind(), meta.RESTScopeNamespace) |
| 61 | + } |
| 62 | + mapper.Add(schema.GroupVersionKind{Version: "v1", Kind: "Pod"}, meta.RESTScopeNamespace) |
| 63 | + return pod, objs, mapper |
| 64 | +} |
| 65 | + |
| 66 | +func walkNodePod(t *testing.T, vmLabels map[string]string) []ObjectID { |
| 67 | + t.Helper() |
| 68 | + pod, objs, mapper := capkNodeChain(vmLabels) |
| 69 | + dyn := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme(), objs...) |
| 70 | + return WalkOwnershipGraph(context.Background(), dyn, mapper, kubernetesAppMapper{}, pod) |
| 71 | +} |
| 72 | + |
| 73 | +func TestWalkResolvesCAPKNodeVMToKubernetesApp(t *testing.T) { |
| 74 | + got := walkNodePod(t, map[string]string{ |
| 75 | + KubevirtMachineNameLabel: "kubernetes-cluster-a-md0-x-y", |
| 76 | + KubevirtMachineNamespaceLabel: capkNamespace, |
| 77 | + }) |
| 78 | + want := ObjectID{APIVersion: "apps.cozystack.io/v1alpha1", Kind: "Kubernetes", Namespace: capkNamespace, Name: "cluster-a"} |
| 79 | + if len(got) != 1 || got[0] != want { |
| 80 | + t.Fatalf("WalkOwnershipGraph(node pod) = %+v, want [%+v]", got, want) |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +func TestWalkIgnoresKubevirtMachineOfAnotherNamespace(t *testing.T) { |
| 85 | + // A VM pointing at a KubevirtMachine outside its own namespace (an external |
| 86 | + // infrastructure cluster, or a forged label) must not be resolved through it. |
| 87 | + got := walkNodePod(t, map[string]string{ |
| 88 | + KubevirtMachineNameLabel: "kubernetes-cluster-a-md0-x-y", |
| 89 | + KubevirtMachineNamespaceLabel: "tenant-other", |
| 90 | + }) |
| 91 | + if len(got) != 0 { |
| 92 | + t.Fatalf("WalkOwnershipGraph(node pod) = %+v, want no ancestors", got) |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +func TestWalkStopsAtUnlabelledVM(t *testing.T) { |
| 97 | + // A standalone VM with no CAPK labels and no ownerReference has no ancestors. |
| 98 | + if got := walkNodePod(t, nil); len(got) != 0 { |
| 99 | + t.Fatalf("WalkOwnershipGraph(pod of standalone VM) = %+v, want no ancestors", got) |
| 100 | + } |
| 101 | +} |
0 commit comments