Ctrl K
ComunidadGuías…

api/handler/agent_kubernetes_bootstrap_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handler

import (
	"context"
	"reflect"
	"testing"

	httputil "github.com/goodrain/rainbond/util/http"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8sfake "k8s.io/client-go/kubernetes/fake"
)

func TestResolveAgentCredentialProfile(t *testing.T) {
	tests := []struct {
		name               string
		request            AgentKubeconfigBootstrapRequest
		wantProfile        string
		wantServiceAccount string
		wantBadRequest     bool
	}{
		{
			name:               "default ops keeps legacy identity",
			request:            AgentKubeconfigBootstrapRequest{},
			wantProfile:        agentCredentialProfileOps,
			wantServiceAccount: "rainbond-agent",
		},
		{
			name: "explicit ops keeps custom identity",
			request: AgentKubeconfigBootstrapRequest{
				CredentialProfile: agentCredentialProfileOps,
				ServiceAccount:    "custom-agent",
			},
			wantProfile:        agentCredentialProfileOps,
			wantServiceAccount: "custom-agent",
		},
		{
			name: "ops rejects fixed reader identity",
			request: AgentKubeconfigBootstrapRequest{
				CredentialProfile: agentCredentialProfileOps,
				ServiceAccount:    agentReaderServiceAccount,
			},
			wantBadRequest: true,
		},
		{
			name: "readonly uses reader identity",
			request: AgentKubeconfigBootstrapRequest{
				CredentialProfile: agentCredentialProfileReadonly,
			},
			wantProfile:        agentCredentialProfileReadonly,
			wantServiceAccount: "rainbond-agent-reader",
		},
		{
			name: "readonly accepts fixed reader identity",
			request: AgentKubeconfigBootstrapRequest{
				CredentialProfile: agentCredentialProfileReadonly,
				ServiceAccount:    "rainbond-agent-reader",
			},
			wantProfile:        agentCredentialProfileReadonly,
			wantServiceAccount: "rainbond-agent-reader",
		},
		{
			name: "readonly rejects legacy ops identity",
			request: AgentKubeconfigBootstrapRequest{
				CredentialProfile: agentCredentialProfileReadonly,
				ServiceAccount:    "rainbond-agent",
			},
			wantBadRequest: true,
		},
		{
			name: "readonly rejects custom identity",
			request: AgentKubeconfigBootstrapRequest{
				CredentialProfile: agentCredentialProfileReadonly,
				ServiceAccount:    "custom-agent",
			},
			wantBadRequest: true,
		},
		{
			name: "unknown profile is rejected",
			request: AgentKubeconfigBootstrapRequest{
				CredentialProfile: "administrator",
			},
			wantBadRequest: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			profile, serviceAccount, err := resolveAgentCredentialProfile(&tt.request)
			if tt.wantBadRequest {
				if err == nil {
					t.Fatal("expected invalid credential profile to be rejected")
				}
				if _, ok := err.(httputil.ErrBadRequest); !ok {
					t.Fatalf("expected bad request error, got %T: %v", err, err)
				}
				return
			}
			if err != nil {
				t.Fatalf("resolve credential profile: %v", err)
			}
			if profile != tt.wantProfile {
				t.Fatalf("expected profile %q, got %q", tt.wantProfile, profile)
			}
			if serviceAccount != tt.wantServiceAccount {
				t.Fatalf("expected service account %q, got %q", tt.wantServiceAccount, serviceAccount)
			}
		})
	}
}

func TestDesiredAgentClusterRoleGrantsClusterWideOperations(t *testing.T) {
	role := desiredAgentClusterRole("rainbond-agent-ops", agentCredentialProfileOps)

	want := []rbacv1.PolicyRule{
		{
			APIGroups: []string{"*"},
			Resources: []string{"*"},
			Verbs:     []string{"*"},
		},
		{
			NonResourceURLs: []string{"*"},
			Verbs:           []string{"get"},
		},
	}

	if !reflect.DeepEqual(role.Rules, want) {
		t.Fatalf("unexpected agent cluster role rules: %#v", role.Rules)
	}
}

func TestDesiredReadonlyAgentClusterRoleAllowsDiagnosticsOnly(t *testing.T) {
	role := desiredAgentClusterRole("rainbond-agent-reader", agentCredentialProfileReadonly)

	allowed := []struct {
		apiGroup string
		resource string
	}{
		{apiGroup: "", resource: "nodes"},
		{apiGroup: "", resource: "namespaces"},
		{apiGroup: "", resource: "pods"},
		{apiGroup: "", resource: "pods/log"},
		{apiGroup: "", resource: "services"},
		{apiGroup: "", resource: "endpoints"},
		{apiGroup: "", resource: "events"},
		{apiGroup: "", resource: "persistentvolumes"},
		{apiGroup: "", resource: "persistentvolumeclaims"},
		{apiGroup: "apps", resource: "deployments"},
		{apiGroup: "apps", resource: "statefulsets"},
		{apiGroup: "apps", resource: "daemonsets"},
		{apiGroup: "apps", resource: "replicasets"},
		{apiGroup: "batch", resource: "jobs"},
		{apiGroup: "batch", resource: "cronjobs"},
		{apiGroup: "networking.k8s.io", resource: "ingresses"},
		{apiGroup: "networking.k8s.io", resource: "networkpolicies"},
		{apiGroup: "metrics.k8s.io", resource: "nodes"},
		{apiGroup: "metrics.k8s.io", resource: "pods"},
	}
	for _, resource := range allowed {
		for _, verb := range []string{"get", "list", "watch"} {
			if !agentRoleAllows(role, resource.apiGroup, resource.resource, verb) {
				t.Errorf("expected readonly role to allow %s %s/%s", verb, resource.apiGroup, resource.resource)
			}
		}
	}

	for _, rule := range role.Rules {
		for _, verb := range rule.Verbs {
			if verb != "get" && verb != "list" && verb != "watch" {
				t.Errorf("readonly role grants forbidden verb %q in rule %#v", verb, rule)
			}
		}
	}
}

func TestDesiredReadonlyAgentClusterRoleRejectsSensitiveOperations(t *testing.T) {
	role := desiredAgentClusterRole("rainbond-agent-reader", agentCredentialProfileReadonly)

	forbiddenResources := []struct {
		apiGroup string
		resource string
	}{
		{apiGroup: "", resource: "secrets"},
		{apiGroup: "", resource: "serviceaccounts"},
		{apiGroup: "", resource: "serviceaccounts/token"},
		{apiGroup: "", resource: "pods/exec"},
		{apiGroup: "", resource: "pods/attach"},
		{apiGroup: "", resource: "pods/portforward"},
	}
	for _, resource := range forbiddenResources {
		for _, verb := range []string{"get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"} {
			if agentRoleAllows(role, resource.apiGroup, resource.resource, verb) {
				t.Errorf("readonly role unexpectedly allows %s %s/%s", verb, resource.apiGroup, resource.resource)
			}
		}
	}

	for _, resource := range []struct {
		apiGroup string
		resource string
	}{
		{apiGroup: "", resource: "pods"},
		{apiGroup: "apps", resource: "deployments"},
		{apiGroup: "batch", resource: "jobs"},
	} {
		for _, verb := range []string{"create", "update", "patch", "delete", "deletecollection"} {
			if agentRoleAllows(role, resource.apiGroup, resource.resource, verb) {
				t.Errorf("readonly role unexpectedly allows %s %s/%s", verb, resource.apiGroup, resource.resource)
			}
		}
	}
}

func TestReconcileAgentRBACRepairsDrift(t *testing.T) {
	desiredRole := desiredAgentClusterRole("rainbond-agent-reader", agentCredentialProfileReadonly)
	existingRole := &rbacv1.ClusterRole{Rules: []rbacv1.PolicyRule{{
		APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"},
	}}}
	if !reconcileAgentClusterRole(existingRole, desiredRole) {
		t.Fatal("expected drifted reader role to be reconciled")
	}
	if !reflect.DeepEqual(existingRole.Rules, desiredRole.Rules) {
		t.Fatalf("reader role rules were not reconciled: %#v", existingRole.Rules)
	}
	if reconcileAgentClusterRole(existingRole, desiredRole) {
		t.Fatal("expected matching reader role to require no update")
	}

	desiredBinding := desiredAgentClusterRoleBinding(
		"rainbond-agent-reader-rbd-system",
		"rainbond-agent-reader",
		"rbd-system",
		"rainbond-agent-reader",
	)
	existingBinding := &rbacv1.ClusterRoleBinding{
		Subjects: []rbacv1.Subject{{Kind: rbacv1.ServiceAccountKind, Name: "rainbond-agent", Namespace: "rbd-system"}},
		RoleRef:  rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "rainbond-agent-ops"},
	}
	if !reconcileAgentClusterRoleBinding(existingBinding, desiredBinding) {
		t.Fatal("expected drifted reader binding to be reconciled")
	}
	if !reflect.DeepEqual(existingBinding.Subjects, desiredBinding.Subjects) || !reflect.DeepEqual(existingBinding.RoleRef, desiredBinding.RoleRef) {
		t.Fatalf("reader binding was not reconciled: %#v", existingBinding)
	}
	if reconcileAgentClusterRoleBinding(existingBinding, desiredBinding) {
		t.Fatal("expected matching reader binding to require no update")
	}
}

func TestAgentRBACProfilesUseIndependentBindings(t *testing.T) {
	opsRoleName, opsBindingName := agentRBACResourceNames(agentCredentialProfileOps, "rbd-system")
	readerRoleName, readerBindingName := agentRBACResourceNames(agentCredentialProfileReadonly, "rbd-system")
	if opsRoleName != "rainbond-agent-ops" || opsBindingName != "rainbond-agent-ops-rbd-system" {
		t.Fatalf("unexpected ops RBAC names: %q/%q", opsRoleName, opsBindingName)
	}
	if readerRoleName != "rainbond-agent-reader" || readerBindingName != "rainbond-agent-reader-rbd-system" {
		t.Fatalf("unexpected reader RBAC names: %q/%q", readerRoleName, readerBindingName)
	}
	if opsRoleName == readerRoleName || opsBindingName == readerBindingName {
		t.Fatalf("expected profile RBAC names to be independent, ops=%q/%q reader=%q/%q", opsRoleName, opsBindingName, readerRoleName, readerBindingName)
	}

	opsBinding := desiredAgentClusterRoleBinding(opsBindingName, opsRoleName, "rbd-system", "rainbond-agent")
	readerBinding := desiredAgentClusterRoleBinding(readerBindingName, readerRoleName, "rbd-system", "rainbond-agent-reader")
	originalOpsBinding := opsBinding.DeepCopy()
	driftedReaderBinding := &rbacv1.ClusterRoleBinding{}
	if !reconcileAgentClusterRoleBinding(driftedReaderBinding, readerBinding) {
		t.Fatal("expected reader binding drift to be reconciled")
	}
	if !reflect.DeepEqual(opsBinding, originalOpsBinding) {
		t.Fatalf("reader reconciliation unexpectedly modified ops binding: %#v", opsBinding)
	}
	if driftedReaderBinding.Name == opsBinding.Name || driftedReaderBinding.RoleRef.Name == opsBinding.RoleRef.Name {
		t.Fatalf("reader binding overlaps ops binding: %#v", driftedReaderBinding)
	}
}

func TestEnsureAgentServiceAccountKeepsProfileBindingsIndependent(t *testing.T) {
	ctx := context.Background()
	clientset := k8sfake.NewSimpleClientset()

	if err := ensureAgentServiceAccountWithClient(ctx, clientset, agentCredentialProfileOps, "rbd-system", agentOpsServiceAccount); err != nil {
		t.Fatalf("ensure ops service account: %v", err)
	}
	opsBinding, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, "rainbond-agent-ops-rbd-system", metav1.GetOptions{})
	if err != nil {
		t.Fatalf("get ops binding: %v", err)
	}
	opsBindingBefore := opsBinding.DeepCopy()

	if err := ensureAgentServiceAccountWithClient(ctx, clientset, agentCredentialProfileReadonly, "rbd-system", agentReaderServiceAccount); err != nil {
		t.Fatalf("ensure reader service account: %v", err)
	}
	for _, serviceAccount := range []string{agentOpsServiceAccount, agentReaderServiceAccount} {
		if _, err := clientset.CoreV1().ServiceAccounts("rbd-system").Get(ctx, serviceAccount, metav1.GetOptions{}); err != nil {
			t.Fatalf("get service account %q: %v", serviceAccount, err)
		}
	}
	readerBinding, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, "rainbond-agent-reader-rbd-system", metav1.GetOptions{})
	if err != nil {
		t.Fatalf("get reader binding: %v", err)
	}
	if len(readerBinding.Subjects) != 1 || readerBinding.Subjects[0].Name != agentReaderServiceAccount {
		t.Fatalf("reader binding targets unexpected subjects: %#v", readerBinding.Subjects)
	}

	readerRole, err := clientset.RbacV1().ClusterRoles().Get(ctx, agentReaderClusterRole, metav1.GetOptions{})
	if err != nil {
		t.Fatalf("get reader role: %v", err)
	}
	readerRole.Rules = []rbacv1.PolicyRule{{APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}}}
	if _, err = clientset.RbacV1().ClusterRoles().Update(ctx, readerRole, metav1.UpdateOptions{}); err != nil {
		t.Fatalf("drift reader role: %v", err)
	}
	readerBinding.Subjects = []rbacv1.Subject{{Kind: rbacv1.ServiceAccountKind, Name: agentOpsServiceAccount, Namespace: "rbd-system"}}
	readerBinding.RoleRef.Name = agentOpsClusterRole
	if _, err = clientset.RbacV1().ClusterRoleBindings().Update(ctx, readerBinding, metav1.UpdateOptions{}); err != nil {
		t.Fatalf("drift reader binding: %v", err)
	}

	if err := ensureAgentServiceAccountWithClient(ctx, clientset, agentCredentialProfileReadonly, "rbd-system", agentReaderServiceAccount); err != nil {
		t.Fatalf("reconcile reader service account: %v", err)
	}
	reconciledReaderRole, err := clientset.RbacV1().ClusterRoles().Get(ctx, agentReaderClusterRole, metav1.GetOptions{})
	if err != nil {
		t.Fatalf("get reconciled reader role: %v", err)
	}
	if !reflect.DeepEqual(reconciledReaderRole.Rules, desiredAgentClusterRole(agentReaderClusterRole, agentCredentialProfileReadonly).Rules) {
		t.Fatalf("reader role drift was not repaired: %#v", reconciledReaderRole.Rules)
	}
	reconciledReaderBinding, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, "rainbond-agent-reader-rbd-system", metav1.GetOptions{})
	if err != nil {
		t.Fatalf("get reconciled reader binding: %v", err)
	}
	if len(reconciledReaderBinding.Subjects) != 1 || reconciledReaderBinding.Subjects[0].Name != agentReaderServiceAccount || reconciledReaderBinding.RoleRef.Name != agentReaderClusterRole {
		t.Fatalf("reader binding drift was not repaired: %#v", reconciledReaderBinding)
	}
	opsBindingAfter, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, "rainbond-agent-ops-rbd-system", metav1.GetOptions{})
	if err != nil {
		t.Fatalf("get ops binding after reader reconciliation: %v", err)
	}
	if !reflect.DeepEqual(opsBindingAfter, opsBindingBefore) {
		t.Fatalf("reader reconciliation modified ops binding: before=%#v after=%#v", opsBindingBefore, opsBindingAfter)
	}
}

func TestEnsureAgentServiceAccountLabelsReconcilesMissingLabels(t *testing.T) {
	serviceAccount := &corev1.ServiceAccount{}

	if !ensureAgentServiceAccountLabels(serviceAccount) {
		t.Fatal("expected missing labels to be reconciled")
	}
	if serviceAccount.Labels["app.kubernetes.io/name"] != "rainbond-agent" {
		t.Fatalf("unexpected app name label: %q", serviceAccount.Labels["app.kubernetes.io/name"])
	}
	if serviceAccount.Labels["app.kubernetes.io/managed-by"] != "rainbond" {
		t.Fatalf("unexpected managed-by label: %q", serviceAccount.Labels["app.kubernetes.io/managed-by"])
	}
	if ensureAgentServiceAccountLabels(serviceAccount) {
		t.Fatal("expected second label reconciliation to be a no-op")
	}
}

func TestDesiredAgentClusterRoleBindingTargetsAgentServiceAccount(t *testing.T) {
	binding := desiredAgentClusterRoleBinding(
		"rainbond-agent-ops-rbd-system",
		"rainbond-agent-ops",
		"rbd-system",
		"rainbond-agent",
	)

	if binding.RoleRef.Kind != "ClusterRole" || binding.RoleRef.Name != "rainbond-agent-ops" {
		t.Fatalf("unexpected role ref: %#v", binding.RoleRef)
	}
	if len(binding.Subjects) != 1 {
		t.Fatalf("expected one subject, got %d", len(binding.Subjects))
	}
	subject := binding.Subjects[0]
	if subject.Kind != rbacv1.ServiceAccountKind || subject.Name != "rainbond-agent" || subject.Namespace != "rbd-system" {
		t.Fatalf("unexpected binding subject: %#v", subject)
	}
}

func agentRoleAllows(role *rbacv1.ClusterRole, apiGroup, resource, verb string) bool {
	for _, rule := range role.Rules {
		if containsAgentRBACValue(rule.APIGroups, apiGroup) &&
			containsAgentRBACValue(rule.Resources, resource) &&
			containsAgentRBACValue(rule.Verbs, verb) {
			return true
		}
	}
	return false
}

func containsAgentRBACValue(values []string, want string) bool {
	for _, value := range values {
		if value == want || value == "*" {
			return true
		}
	}
	return false
}