feat(depends): add RBAC
This commit is contained in:
@@ -9,8 +9,10 @@ import (
|
||||
"katenary.io/internal/generator/labels"
|
||||
|
||||
yaml3 "gopkg.in/yaml.v3"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
@@ -643,3 +645,197 @@ services:
|
||||
t.Errorf("Expected command to be 'bar baz', got %s", strings.Join(command, " "))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestrictedRBACGeneration(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
ports:
|
||||
- 80:80
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
database:
|
||||
image: mariadb:10.5
|
||||
ports:
|
||||
- 3306:3306
|
||||
`
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
rbacOutput := internalCompileTest(t, "-s", "templates/web/depends-on.rbac.yaml")
|
||||
|
||||
docs := strings.Split(rbacOutput, "---\n")
|
||||
|
||||
// Filter out empty documents and strip helm template comments
|
||||
var filteredDocs []string
|
||||
for _, doc := range docs {
|
||||
if strings.TrimSpace(doc) != "" {
|
||||
// Remove '# Source:' comment lines that helm template adds
|
||||
lines := strings.Split(doc, "\n")
|
||||
var contentLines []string
|
||||
for _, line := range lines {
|
||||
if !strings.HasPrefix(strings.TrimSpace(line), "# Source:") {
|
||||
contentLines = append(contentLines, line)
|
||||
}
|
||||
}
|
||||
filteredDocs = append(filteredDocs, strings.Join(contentLines, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredDocs) != 3 {
|
||||
t.Fatalf("Expected 3 YAML documents in RBAC file, got %d (filtered from %d)", len(filteredDocs), len(docs))
|
||||
}
|
||||
|
||||
var sa corev1.ServiceAccount
|
||||
if err := yaml.Unmarshal([]byte(strings.TrimSpace(filteredDocs[0])), &sa); err != nil {
|
||||
t.Errorf("Failed to unmarshal ServiceAccount: %v", err)
|
||||
}
|
||||
if sa.Kind != "ServiceAccount" {
|
||||
t.Errorf("Expected Kind=ServiceAccount, got %s", sa.Kind)
|
||||
}
|
||||
if !strings.Contains(sa.Name, "web") {
|
||||
t.Errorf("Expected ServiceAccount name to contain 'web', got %s", sa.Name)
|
||||
}
|
||||
|
||||
var role rbacv1.Role
|
||||
if err := yaml.Unmarshal([]byte(strings.TrimSpace(filteredDocs[1])), &role); err != nil {
|
||||
t.Errorf("Failed to unmarshal Role: %v", err)
|
||||
}
|
||||
if role.Kind != "Role" {
|
||||
t.Errorf("Expected Kind=Role, got %s", role.Kind)
|
||||
}
|
||||
if len(role.Rules) != 1 {
|
||||
t.Errorf("Expected 1 rule in Role, got %d", len(role.Rules))
|
||||
}
|
||||
|
||||
rule := role.Rules[0]
|
||||
if !contains(rule.APIGroups, "") {
|
||||
t.Error("Expected APIGroup to include core API ('')")
|
||||
}
|
||||
if !contains(rule.Resources, "endpoints") {
|
||||
t.Errorf("Expected Resource to include 'endpoints', got %v", rule.Resources)
|
||||
}
|
||||
|
||||
for _, res := range rule.Resources {
|
||||
if res == "*" {
|
||||
t.Error("Role should not have wildcard (*) resource permissions")
|
||||
}
|
||||
}
|
||||
for _, verb := range rule.Verbs {
|
||||
if verb == "*" {
|
||||
t.Error("Role should not have wildcard (*) verb permissions")
|
||||
}
|
||||
}
|
||||
|
||||
var rb rbacv1.RoleBinding
|
||||
if err := yaml.Unmarshal([]byte(strings.TrimSpace(filteredDocs[2])), &rb); err != nil {
|
||||
t.Errorf("Failed to unmarshal RoleBinding: %v", err)
|
||||
}
|
||||
if rb.Kind != "RoleBinding" {
|
||||
t.Errorf("Expected Kind=RoleBinding, got %s", rb.Kind)
|
||||
}
|
||||
if len(rb.Subjects) != 1 {
|
||||
t.Errorf("Expected 1 subject in RoleBinding, got %d", len(rb.Subjects))
|
||||
}
|
||||
if rb.Subjects[0].Kind != "ServiceAccount" {
|
||||
t.Errorf("Expected Subject Kind=ServiceAccount, got %s", rb.Subjects[0].Kind)
|
||||
}
|
||||
|
||||
// Helm template renders the name, so check if it contains "web"
|
||||
if !strings.Contains(rb.RoleRef.Name, "web") {
|
||||
t.Errorf("Expected RoleRef Name to contain 'web', got %s", rb.RoleRef.Name)
|
||||
}
|
||||
if rb.RoleRef.Kind != "Role" {
|
||||
t.Errorf("Expected RoleRef Kind=Role, got %s", rb.RoleRef.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeploymentReferencesServiceAccount(t *testing.T) {
|
||||
composeFile := `
|
||||
services:
|
||||
web:
|
||||
image: nginx:1.29
|
||||
ports:
|
||||
- 80:80
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
database:
|
||||
image: mariadb:10.5
|
||||
ports:
|
||||
- 3306:3306
|
||||
`
|
||||
tmpDir := setup(composeFile)
|
||||
defer teardown(tmpDir)
|
||||
|
||||
currentDir, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(currentDir)
|
||||
|
||||
output := internalCompileTest(t, "-s", "templates/web/deployment.yaml")
|
||||
|
||||
var dt appsv1.Deployment
|
||||
if err := yaml.Unmarshal([]byte(output), &dt); err != nil {
|
||||
t.Errorf("Failed to unmarshal Deployment: %v", err)
|
||||
}
|
||||
|
||||
serviceAccountName := dt.Spec.Template.Spec.ServiceAccountName
|
||||
if !strings.Contains(serviceAccountName, "web") {
|
||||
t.Errorf("Expected ServiceAccountName to contain 'web', got %s", serviceAccountName)
|
||||
}
|
||||
|
||||
if len(dt.Spec.Template.Spec.InitContainers) == 0 {
|
||||
t.Fatal("Expected at least one init container for depends_on")
|
||||
}
|
||||
|
||||
initContainer := dt.Spec.Template.Spec.InitContainers[0]
|
||||
if initContainer.Name != "wait-for-database" {
|
||||
t.Errorf("Expected init container name 'wait-for-database', got %s", initContainer.Name)
|
||||
}
|
||||
|
||||
fullCommand := strings.Join(initContainer.Command, " ")
|
||||
if !strings.Contains(fullCommand, "wget") {
|
||||
t.Error("Expected init container to use wget for K8s API calls")
|
||||
}
|
||||
if !strings.Contains(fullCommand, "/api/v1/namespaces/") {
|
||||
t.Error("Expected init container to call /api/v1/namespaces/ endpoint")
|
||||
}
|
||||
if !strings.Contains(fullCommand, "/endpoints/") {
|
||||
t.Error("Expected init container to access /endpoints/ resource")
|
||||
}
|
||||
|
||||
hasNamespace := false
|
||||
for _, env := range initContainer.Env {
|
||||
if env.Name == "NAMESPACE" && env.ValueFrom != nil && env.ValueFrom.FieldRef != nil {
|
||||
if env.ValueFrom.FieldRef.FieldPath == "metadata.namespace" {
|
||||
hasNamespace = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasNamespace {
|
||||
t.Error("Expected NAMESPACE env var with metadata.namespace fieldRef")
|
||||
}
|
||||
|
||||
_, err := os.Stat("./chart/templates/web/depends-on.rbac.yaml")
|
||||
if os.IsNotExist(err) {
|
||||
t.Error("RBAC file depends-on.rbac.yaml should exist for service using depends_on with K8s API")
|
||||
} else if err != nil {
|
||||
t.Errorf("Unexpected error checking RBAC file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user