Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 52 additions & 26 deletions internal/addons/cluster_autoscaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,14 +585,17 @@ func (c *ClusterAutoscalerInstaller) buildKubeletArgsString() string {
return strings.Join(args, " ")
}

// patchClusterRole patches the ClusterRole to add volumeattachments permission
// patchClusterRole patches the ClusterRole to add missing permissions
func (c *ClusterAutoscalerInstaller) patchClusterRole(doc map[string]interface{}) {
rules, ok := doc["rules"].([]interface{})
if !ok {
return
}

// Find the storage.k8s.io rule and add volumeattachments if not present
hasStorageRule := false
hasResourceRule := false

// Find existing rules and patch as needed
for _, rule := range rules {
ruleMap, ok := rule.(map[string]interface{})
if !ok {
Expand All @@ -604,35 +607,58 @@ func (c *ClusterAutoscalerInstaller) patchClusterRole(doc map[string]interface{}
continue
}

hasStorageAPI := false
for _, group := range apiGroups {
if groupStr, ok := group.(string); ok && groupStr == "storage.k8s.io" {
hasStorageAPI = true
break
groupStr, ok := group.(string)
if !ok {
continue
}
}

if !hasStorageAPI {
continue
}

resources, ok := ruleMap["resources"].([]interface{})
if !ok {
continue
}

// Check if volumeattachments is already present
hasVolumeAttachments := false
for _, res := range resources {
if resStr, ok := res.(string); ok && resStr == "volumeattachments" {
hasVolumeAttachments = true
break
switch groupStr {
case "storage.k8s.io":
hasStorageRule = true
resources, ok := ruleMap["resources"].([]interface{})
if !ok {
continue
}

// Check if volumeattachments is already present
hasVolumeAttachments := false
for _, res := range resources {
if resStr, ok := res.(string); ok && resStr == "volumeattachments" {
hasVolumeAttachments = true
break
}
}

if !hasVolumeAttachments {
resources = append(resources, "volumeattachments")
ruleMap["resources"] = resources
}

case "resource.k8s.io":
hasResourceRule = true
}
}
}

if !hasVolumeAttachments {
resources = append(resources, "volumeattachments")
ruleMap["resources"] = resources
}
// Add storage.k8s.io rule with volumeattachments if no storage rule exists at all
if !hasStorageRule {
rules = append(rules, map[string]interface{}{
"apiGroups": []interface{}{"storage.k8s.io"},
"resources": []interface{}{"volumeattachments"},
"verbs": []interface{}{"get", "list", "watch"},
})
}

// Add resource.k8s.io rule for Dynamic Resource Allocation (DRA) resources
// Required by cluster-autoscaler using client-go v0.35.0+ (Kubernetes 1.32+)
if !hasResourceRule {
rules = append(rules, map[string]interface{}{
"apiGroups": []interface{}{"resource.k8s.io"},
"resources": []interface{}{"resourceclaims", "resourceslices", "deviceclasses"},
"verbs": []interface{}{"get", "list", "watch"},
})
}

doc["rules"] = rules
}
160 changes: 160 additions & 0 deletions internal/addons/cluster_autoscaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,166 @@ func TestSSHKeyEnvironmentVariable(t *testing.T) {
}
}

// TestPatchClusterRole verifies that patchClusterRole adds required RBAC permissions
func TestPatchClusterRole(t *testing.T) {
installer := &ClusterAutoscalerInstaller{}

t.Run("adds resource.k8s.io rule when missing", func(t *testing.T) {
doc := map[string]interface{}{
"kind": "ClusterRole",
"rules": []interface{}{
map[string]interface{}{
"apiGroups": []interface{}{"storage.k8s.io"},
"resources": []interface{}{"storageclasses", "volumeattachments"},
"verbs": []interface{}{"get", "list", "watch"},
},
},
}

installer.patchClusterRole(doc)

rules, ok := doc["rules"].([]interface{})
if !ok {
t.Fatal("rules is not a slice")
}

var hasResourceRule bool
var resourceRuleResources []string
for _, rule := range rules {
ruleMap, ok := rule.(map[string]interface{})
if !ok {
continue
}
apiGroups, ok := ruleMap["apiGroups"].([]interface{})
if !ok {
continue
}
for _, g := range apiGroups {
if g == "resource.k8s.io" {
hasResourceRule = true
resList, ok := ruleMap["resources"].([]interface{})
if !ok {
t.Fatal("resources in resource.k8s.io rule is not a slice")
}
for _, r := range resList {
resStr, ok := r.(string)
if !ok {
t.Fatalf("resource entry is not a string: %v", r)
}
resourceRuleResources = append(resourceRuleResources, resStr)
}
}
}
}

if !hasResourceRule {
t.Error("expected resource.k8s.io rule to be added, but it was not found")
}

for _, expected := range []string{"resourceclaims", "resourceslices", "deviceclasses"} {
found := false
for _, res := range resourceRuleResources {
if res == expected {
found = true
break
}
}
if !found {
t.Errorf("expected resource %q in resource.k8s.io rule, but not found; got: %v", expected, resourceRuleResources)
}
}
})

t.Run("does not duplicate resource.k8s.io rule when already present", func(t *testing.T) {
doc := map[string]interface{}{
"kind": "ClusterRole",
"rules": []interface{}{
map[string]interface{}{
"apiGroups": []interface{}{"resource.k8s.io"},
"resources": []interface{}{"resourceclaims", "resourceslices", "deviceclasses"},
"verbs": []interface{}{"get", "list", "watch"},
},
},
}

installer.patchClusterRole(doc)

rules, ok := doc["rules"].([]interface{})
if !ok {
t.Fatal("rules is not a slice")
}

count := 0
for _, rule := range rules {
ruleMap, ok := rule.(map[string]interface{})
if !ok {
continue
}
apiGroups, ok := ruleMap["apiGroups"].([]interface{})
if !ok {
continue
}
for _, g := range apiGroups {
if g == "resource.k8s.io" {
count++
}
}
}

if count != 1 {
t.Errorf("expected exactly 1 resource.k8s.io rule, got %d", count)
}
})

t.Run("adds volumeattachments to existing storage.k8s.io rule", func(t *testing.T) {
doc := map[string]interface{}{
"kind": "ClusterRole",
"rules": []interface{}{
map[string]interface{}{
"apiGroups": []interface{}{"storage.k8s.io"},
"resources": []interface{}{"storageclasses"},
"verbs": []interface{}{"get", "list", "watch"},
},
},
}

installer.patchClusterRole(doc)

rules, ok := doc["rules"].([]interface{})
if !ok {
t.Fatal("rules is not a slice")
}

for _, rule := range rules {
ruleMap, ok := rule.(map[string]interface{})
if !ok {
continue
}
apiGroups, ok := ruleMap["apiGroups"].([]interface{})
if !ok {
continue
}
for _, g := range apiGroups {
if g == "storage.k8s.io" {
resources, ok := ruleMap["resources"].([]interface{})
if !ok {
t.Fatal("resources is not a slice")
}
found := false
for _, r := range resources {
if r == "volumeattachments" {
found = true
}
}
if !found {
t.Error("expected volumeattachments to be added to storage.k8s.io rule")
}
}
}
}
})
}

// stringPtr is a helper function to create string pointers for test data
func stringPtr(s string) *string {
return &s
Expand Down
Loading