api/handler/agent_kubernetes_bootstrap_test.go
Rama main · réplica de lectura del repositorio original.
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
}