462 lines
14 KiB
Go
462 lines
14 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
|
"github.com/stretchr/testify/assert"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
)
|
|
|
|
// Helper functions to create test resources
|
|
func createTestDeployment(name, namespace string, replicas int32) *appsv1.Deployment {
|
|
return &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
UID: types.UID("deploy-" + name),
|
|
Labels: map[string]string{
|
|
"app": name,
|
|
},
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Replicas: &replicas,
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": name,
|
|
},
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"app": name,
|
|
},
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: name,
|
|
Image: "nginx:latest",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{},
|
|
Requests: corev1.ResourceList{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Status: appsv1.DeploymentStatus{
|
|
Replicas: replicas,
|
|
ReadyReplicas: replicas,
|
|
},
|
|
}
|
|
}
|
|
|
|
func createTestReplicaSet(name, namespace, deploymentName string) *appsv1.ReplicaSet {
|
|
return &appsv1.ReplicaSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
UID: types.UID("rs-" + name),
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
Kind: "Deployment",
|
|
Name: deploymentName,
|
|
UID: types.UID("deploy-" + deploymentName),
|
|
},
|
|
},
|
|
},
|
|
Spec: appsv1.ReplicaSetSpec{
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": deploymentName,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func createTestStatefulSet(name, namespace string, replicas int32) *appsv1.StatefulSet {
|
|
return &appsv1.StatefulSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
UID: types.UID("sts-" + name),
|
|
Labels: map[string]string{
|
|
"app": name,
|
|
},
|
|
},
|
|
Spec: appsv1.StatefulSetSpec{
|
|
Replicas: &replicas,
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": name,
|
|
},
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"app": name,
|
|
},
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: name,
|
|
Image: "redis:latest",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{},
|
|
Requests: corev1.ResourceList{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Status: appsv1.StatefulSetStatus{
|
|
Replicas: replicas,
|
|
ReadyReplicas: replicas,
|
|
},
|
|
}
|
|
}
|
|
|
|
func createTestDaemonSet(name, namespace string) *appsv1.DaemonSet {
|
|
return &appsv1.DaemonSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
UID: types.UID("ds-" + name),
|
|
Labels: map[string]string{
|
|
"app": name,
|
|
},
|
|
},
|
|
Spec: appsv1.DaemonSetSpec{
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": name,
|
|
},
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Labels: map[string]string{
|
|
"app": name,
|
|
},
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: name,
|
|
Image: "fluentd:latest",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{},
|
|
Requests: corev1.ResourceList{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Status: appsv1.DaemonSetStatus{
|
|
DesiredNumberScheduled: 2,
|
|
NumberReady: 2,
|
|
},
|
|
}
|
|
}
|
|
|
|
func createTestPod(name, namespace, ownerKind, ownerName string, isRunning bool) *corev1.Pod {
|
|
phase := corev1.PodPending
|
|
if isRunning {
|
|
phase = corev1.PodRunning
|
|
}
|
|
|
|
var ownerReferences []metav1.OwnerReference
|
|
if ownerKind != "" && ownerName != "" {
|
|
ownerReferences = []metav1.OwnerReference{
|
|
{
|
|
Kind: ownerKind,
|
|
Name: ownerName,
|
|
UID: types.UID(ownerKind + "-" + ownerName),
|
|
},
|
|
}
|
|
}
|
|
|
|
return &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
UID: types.UID("pod-" + name),
|
|
OwnerReferences: ownerReferences,
|
|
Labels: map[string]string{
|
|
"app": ownerName,
|
|
},
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "container-" + name,
|
|
Image: "busybox:latest",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{},
|
|
Requests: corev1.ResourceList{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Status: corev1.PodStatus{
|
|
Phase: phase,
|
|
},
|
|
}
|
|
}
|
|
|
|
func createTestService(name, namespace string, selector map[string]string) *corev1.Service {
|
|
return &corev1.Service{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
UID: types.UID("svc-" + name),
|
|
},
|
|
Spec: corev1.ServiceSpec{
|
|
Selector: selector,
|
|
Type: corev1.ServiceTypeClusterIP,
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestGetApplications(t *testing.T) {
|
|
t.Run("Admin user - Mix of deployments, statefulsets and daemonsets with and without pods", func(t *testing.T) {
|
|
// Create a fake K8s client
|
|
fakeClient := fake.NewSimpleClientset()
|
|
|
|
// Setup the test namespace
|
|
namespace := "test-namespace"
|
|
defaultNamespace := "default"
|
|
|
|
// Create resources in the test namespace
|
|
// 1. Deployment with pods
|
|
deployWithPods := createTestDeployment("deploy-with-pods", namespace, 2)
|
|
_, err := fakeClient.AppsV1().Deployments(namespace).Create(context.TODO(), deployWithPods, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
replicaSet := createTestReplicaSet("rs-deploy-with-pods", namespace, "deploy-with-pods")
|
|
_, err = fakeClient.AppsV1().ReplicaSets(namespace).Create(context.TODO(), replicaSet, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
pod1 := createTestPod("pod1-deploy", namespace, "ReplicaSet", "rs-deploy-with-pods", true)
|
|
_, err = fakeClient.CoreV1().Pods(namespace).Create(context.TODO(), pod1, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
pod2 := createTestPod("pod2-deploy", namespace, "ReplicaSet", "rs-deploy-with-pods", true)
|
|
_, err = fakeClient.CoreV1().Pods(namespace).Create(context.TODO(), pod2, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// 2. Deployment without pods (scaled to 0)
|
|
deployNoPods := createTestDeployment("deploy-no-pods", namespace, 0)
|
|
_, err = fakeClient.AppsV1().Deployments(namespace).Create(context.TODO(), deployNoPods, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// 3. StatefulSet with pods
|
|
stsWithPods := createTestStatefulSet("sts-with-pods", namespace, 1)
|
|
_, err = fakeClient.AppsV1().StatefulSets(namespace).Create(context.TODO(), stsWithPods, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
pod3 := createTestPod("pod1-sts", namespace, "StatefulSet", "sts-with-pods", true)
|
|
_, err = fakeClient.CoreV1().Pods(namespace).Create(context.TODO(), pod3, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// 4. StatefulSet without pods
|
|
stsNoPods := createTestStatefulSet("sts-no-pods", namespace, 0)
|
|
_, err = fakeClient.AppsV1().StatefulSets(namespace).Create(context.TODO(), stsNoPods, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// 5. DaemonSet with pods
|
|
dsWithPods := createTestDaemonSet("ds-with-pods", namespace)
|
|
_, err = fakeClient.AppsV1().DaemonSets(namespace).Create(context.TODO(), dsWithPods, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
pod4 := createTestPod("pod1-ds", namespace, "DaemonSet", "ds-with-pods", true)
|
|
_, err = fakeClient.CoreV1().Pods(namespace).Create(context.TODO(), pod4, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
pod5 := createTestPod("pod2-ds", namespace, "DaemonSet", "ds-with-pods", true)
|
|
_, err = fakeClient.CoreV1().Pods(namespace).Create(context.TODO(), pod5, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// 6. Naked Pod (no owner reference)
|
|
nakedPod := createTestPod("naked-pod", namespace, "", "", true)
|
|
_, err = fakeClient.CoreV1().Pods(namespace).Create(context.TODO(), nakedPod, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// 7. Resources in another namespace
|
|
deployOtherNs := createTestDeployment("deploy-other-ns", defaultNamespace, 1)
|
|
_, err = fakeClient.AppsV1().Deployments(defaultNamespace).Create(context.TODO(), deployOtherNs, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
podOtherNs := createTestPod("pod-other-ns", defaultNamespace, "Deployment", "deploy-other-ns", true)
|
|
_, err = fakeClient.CoreV1().Pods(defaultNamespace).Create(context.TODO(), podOtherNs, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// 8. Add a service (dependency)
|
|
service := createTestService("svc-deploy", namespace, map[string]string{"app": "deploy-with-pods"})
|
|
_, err = fakeClient.CoreV1().Services(namespace).Create(context.TODO(), service, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// Create the KubeClient with admin privileges
|
|
kubeClient := &KubeClient{
|
|
cli: fakeClient,
|
|
instanceID: "test-instance",
|
|
IsKubeAdmin: true,
|
|
}
|
|
|
|
// Test cases
|
|
|
|
// 1. All resources, no filtering
|
|
t.Run("All resources with dependencies", func(t *testing.T) {
|
|
apps, err := kubeClient.GetApplications("", "")
|
|
assert.NoError(t, err)
|
|
|
|
// We expect 7 resources: 2 deployments + 2 statefulsets + 1 daemonset + 1 naked pod + 1 deployment in other namespace
|
|
// Note: Each controller with pods should count once, not per pod
|
|
assert.Equal(t, 7, len(apps))
|
|
|
|
// Verify one of the deployments has services attached
|
|
appsWithServices := []models.K8sApplication{}
|
|
for _, app := range apps {
|
|
if len(app.Services) > 0 {
|
|
appsWithServices = append(appsWithServices, app)
|
|
}
|
|
}
|
|
assert.Equal(t, 1, len(appsWithServices))
|
|
assert.Equal(t, "deploy-with-pods", appsWithServices[0].Name)
|
|
})
|
|
|
|
// 2. Filter by namespace
|
|
t.Run("Filter by namespace", func(t *testing.T) {
|
|
apps, err := kubeClient.GetApplications(namespace, "")
|
|
assert.NoError(t, err)
|
|
|
|
// We expect 6 resources in the test namespace
|
|
assert.Equal(t, 6, len(apps))
|
|
|
|
// Verify resources from other namespaces are not included
|
|
for _, app := range apps {
|
|
assert.Equal(t, namespace, app.ResourcePool)
|
|
}
|
|
})
|
|
})
|
|
|
|
t.Run("Non-admin user - Resources filtered by accessible namespaces", func(t *testing.T) {
|
|
// Create a fake K8s client
|
|
fakeClient := fake.NewSimpleClientset()
|
|
|
|
// Setup the test namespaces
|
|
namespace1 := "allowed-ns"
|
|
namespace2 := "restricted-ns"
|
|
|
|
// Create resources in the allowed namespace
|
|
sts1 := createTestStatefulSet("sts-allowed", namespace1, 1)
|
|
_, err := fakeClient.AppsV1().StatefulSets(namespace1).Create(context.TODO(), sts1, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
pod1 := createTestPod("pod-allowed", namespace1, "StatefulSet", "sts-allowed", true)
|
|
_, err = fakeClient.CoreV1().Pods(namespace1).Create(context.TODO(), pod1, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// Add a StatefulSet without pods in the allowed namespace
|
|
stsNoPods := createTestStatefulSet("sts-no-pods-allowed", namespace1, 0)
|
|
_, err = fakeClient.AppsV1().StatefulSets(namespace1).Create(context.TODO(), stsNoPods, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// Create resources in the restricted namespace
|
|
sts2 := createTestStatefulSet("sts-restricted", namespace2, 1)
|
|
_, err = fakeClient.AppsV1().StatefulSets(namespace2).Create(context.TODO(), sts2, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
pod2 := createTestPod("pod-restricted", namespace2, "StatefulSet", "sts-restricted", true)
|
|
_, err = fakeClient.CoreV1().Pods(namespace2).Create(context.TODO(), pod2, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// Create the KubeClient with non-admin privileges (only allowed namespace1)
|
|
kubeClient := &KubeClient{
|
|
cli: fakeClient,
|
|
instanceID: "test-instance",
|
|
IsKubeAdmin: false,
|
|
NonAdminNamespaces: []string{namespace1},
|
|
}
|
|
|
|
// Test that only resources from allowed namespace are returned
|
|
apps, err := kubeClient.GetApplications("", "")
|
|
assert.NoError(t, err)
|
|
|
|
// We expect 2 resources from the allowed namespace (1 sts with pod + 1 sts without pod)
|
|
assert.Equal(t, 2, len(apps))
|
|
|
|
// Verify resources are from the allowed namespace
|
|
for _, app := range apps {
|
|
assert.Equal(t, namespace1, app.ResourcePool)
|
|
assert.Equal(t, "StatefulSet", app.Kind)
|
|
}
|
|
|
|
// Verify names of returned resources
|
|
stsNames := make(map[string]bool)
|
|
for _, app := range apps {
|
|
stsNames[app.Name] = true
|
|
}
|
|
|
|
assert.True(t, stsNames["sts-allowed"], "Expected StatefulSet 'sts-allowed' was not found")
|
|
assert.True(t, stsNames["sts-no-pods-allowed"], "Expected StatefulSet 'sts-no-pods-allowed' was not found")
|
|
})
|
|
|
|
t.Run("Filter by node name", func(t *testing.T) {
|
|
// Create a fake K8s client
|
|
fakeClient := fake.NewSimpleClientset()
|
|
|
|
// Setup test namespace
|
|
namespace := "node-filter-ns"
|
|
nodeName := "worker-node-1"
|
|
|
|
// Create a deployment with pods on specific node
|
|
deploy := createTestDeployment("node-deploy", namespace, 2)
|
|
_, err := fakeClient.AppsV1().Deployments(namespace).Create(context.TODO(), deploy, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// Create ReplicaSet for the deployment
|
|
rs := createTestReplicaSet("rs-node-deploy", namespace, "node-deploy")
|
|
_, err = fakeClient.AppsV1().ReplicaSets(namespace).Create(context.TODO(), rs, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// Create 2 pods, one on the specified node, one on a different node
|
|
pod1 := createTestPod("pod-on-node", namespace, "ReplicaSet", "rs-node-deploy", true)
|
|
pod1.Spec.NodeName = nodeName
|
|
_, err = fakeClient.CoreV1().Pods(namespace).Create(context.TODO(), pod1, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
pod2 := createTestPod("pod-other-node", namespace, "ReplicaSet", "rs-node-deploy", true)
|
|
pod2.Spec.NodeName = "worker-node-2"
|
|
_, err = fakeClient.CoreV1().Pods(namespace).Create(context.TODO(), pod2, metav1.CreateOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
// Create the KubeClient
|
|
kubeClient := &KubeClient{
|
|
cli: fakeClient,
|
|
instanceID: "test-instance",
|
|
IsKubeAdmin: true,
|
|
}
|
|
|
|
// Test filtering by node name
|
|
apps, err := kubeClient.GetApplications(namespace, nodeName)
|
|
assert.NoError(t, err)
|
|
|
|
// We expect to find only the pod on the specified node
|
|
assert.Equal(t, 1, len(apps))
|
|
if len(apps) > 0 {
|
|
assert.Equal(t, "node-deploy", apps[0].Name)
|
|
}
|
|
})
|
|
}
|