Skip to content

Commit 90826c4

Browse files
CopilotdevantlerCopilot
authored
feat: implement RoleGenerator with custom models and type-safe RoleVerb enum (#366)
* Initial plan * feat: implement RoleGenerator with custom models using BaseKubernetesGenerator Co-authored-by: devantler <[email protected]> * feat: complete RoleGenerator implementation with comprehensive test coverage Co-authored-by: devantler <[email protected]> * refactor: rename ClusterRoleVerb to RoleVerb for better semantic clarity Co-authored-by: devantler <[email protected]> * Update src/DevantlerTech.KubernetesGenerator.Native/Models/Role.cs Co-authored-by: Copilot <[email protected]> Signed-off-by: Nikolai Emil Damm <[email protected]> --------- Signed-off-by: Nikolai Emil Damm <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: devantler <[email protected]> Co-authored-by: Nikolai Emil Damm <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 2e9283b commit 90826c4

File tree

9 files changed

+254
-38
lines changed

9 files changed

+254
-38
lines changed

src/DevantlerTech.KubernetesGenerator.Native/Models/ClusterRoleRule.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class ClusterRoleRule
88
/// <summary>
99
/// Gets or sets the verbs that apply to the resources.
1010
/// </summary>
11-
public required IList<ClusterRoleVerb> Verbs { get; init; } = [];
11+
public required IList<RoleVerb> Verbs { get; init; } = [];
1212

1313
/// <summary>
1414
/// Gets or sets the API groups that the rule applies to.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace DevantlerTech.KubernetesGenerator.Native.Models;
2+
3+
/// <summary>
4+
/// Represents a role for Kubernetes RBAC with type-safe options.
5+
/// Uses BaseKubernetesGenerator to generate Kubernetes RBAC roles programmatically.
6+
/// </summary>
7+
public class Role
8+
{
9+
/// <summary>
10+
/// Gets or sets the API version for the role.
11+
/// </summary>
12+
public string ApiVersion { get; set; } = "rbac.authorization.k8s.io/v1";
13+
14+
/// <summary>
15+
/// Gets or sets the kind of the resource.
16+
/// </summary>
17+
public string Kind { get; set; } = "Role";
18+
19+
/// <summary>
20+
/// Gets or sets the metadata for the role.
21+
/// </summary>
22+
public required Metadata Metadata { get; set; }
23+
24+
/// <summary>
25+
/// Gets or sets the rules that define the permissions.
26+
/// </summary>
27+
public IList<RoleRule>? Rules { get; init; }
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace DevantlerTech.KubernetesGenerator.Native.Models;
2+
3+
/// <summary>
4+
/// Represents a rule for role permissions with type-safe verbs.
5+
/// </summary>
6+
public class RoleRule
7+
{
8+
/// <summary>
9+
/// Gets or sets the verbs that apply to the resources.
10+
/// </summary>
11+
public required IList<RoleVerb> Verbs { get; init; } = [];
12+
13+
/// <summary>
14+
/// Gets or sets the API groups that the rule applies to.
15+
/// Empty string "" means the core API group.
16+
/// </summary>
17+
public IList<string>? ApiGroups { get; init; }
18+
19+
/// <summary>
20+
/// Gets or sets the resources that the rule applies to.
21+
/// </summary>
22+
public IList<string>? Resources { get; init; }
23+
24+
/// <summary>
25+
/// Gets or sets specific resource names that the rule applies to.
26+
/// If empty, applies to all resources of the specified type.
27+
/// </summary>
28+
public IList<string>? ResourceNames { get; init; }
29+
}

src/DevantlerTech.KubernetesGenerator.Native/Models/ClusterRoleVerb.cs renamed to src/DevantlerTech.KubernetesGenerator.Native/Models/RoleVerb.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
namespace DevantlerTech.KubernetesGenerator.Native.Models;
44

55
/// <summary>
6-
/// Represents the verbs that can be used in ClusterRole rules.
6+
/// Represents the verbs that can be used in Role and ClusterRole rules.
77
/// </summary>
8-
public enum ClusterRoleVerb
8+
public enum RoleVerb
99
{
1010
/// <summary>
1111
/// Read the specified resource.
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
using DevantlerTech.KubernetesGenerator.Core;
2-
using k8s.Models;
2+
using DevantlerTech.KubernetesGenerator.Native.Models;
33

44
namespace DevantlerTech.KubernetesGenerator.Native;
55

66
/// <summary>
7-
/// A generator for Kubernetes Role objects.
7+
/// A generator for Kubernetes Role objects using custom models with type-safe options.
8+
/// Uses BaseKubernetesGenerator since kubectl create role requires API server connectivity.
89
/// </summary>
9-
public class RoleGenerator : BaseKubernetesGenerator<V1Role>
10+
public class RoleGenerator : BaseKubernetesGenerator<Role>
1011
{
1112
}

tests/DevantlerTech.KubernetesGenerator.Native.Tests/ClusterRoleGeneratorTests/GenerateAsyncTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public async Task GenerateAsync_WithBasicRule_ShouldGenerateAValidClusterRole()
2626
[
2727
new ClusterRoleRule
2828
{
29-
Verbs = [ClusterRoleVerb.Get, ClusterRoleVerb.List, ClusterRoleVerb.Watch],
29+
Verbs = [RoleVerb.Get, RoleVerb.List, RoleVerb.Watch],
3030
ApiGroups = [""],
3131
Resources = ["pods"]
3232
}
@@ -67,7 +67,7 @@ public async Task GenerateAsync_WithResourceNames_ShouldGenerateAValidClusterRol
6767
[
6868
new ClusterRoleRule
6969
{
70-
Verbs = [ClusterRoleVerb.Get],
70+
Verbs = [RoleVerb.Get],
7171
ApiGroups = [""],
7272
Resources = ["pods"],
7373
ResourceNames = ["my-pod", "another-pod"]
@@ -109,7 +109,7 @@ public async Task GenerateAsync_WithApiGroups_ShouldGenerateAValidClusterRole()
109109
[
110110
new ClusterRoleRule
111111
{
112-
Verbs = [ClusterRoleVerb.Get, ClusterRoleVerb.List, ClusterRoleVerb.Watch],
112+
Verbs = [RoleVerb.Get, RoleVerb.List, RoleVerb.Watch],
113113
ApiGroups = ["apps"],
114114
Resources = ["replicasets"]
115115
}
@@ -150,7 +150,7 @@ public async Task GenerateAsync_WithNonResourceUrls_ShouldGenerateAValidClusterR
150150
[
151151
new ClusterRoleRule
152152
{
153-
Verbs = [ClusterRoleVerb.Get],
153+
Verbs = [RoleVerb.Get],
154154
NonResourceURLs = ["/logs/*", "/metrics"]
155155
}
156156
]
@@ -244,20 +244,20 @@ public async Task GenerateAsync_WithComprehensiveFeatures_ShouldGenerateAValidCl
244244
[
245245
new ClusterRoleRule
246246
{
247-
Verbs = [ClusterRoleVerb.Get, ClusterRoleVerb.List, ClusterRoleVerb.Watch],
247+
Verbs = [RoleVerb.Get, RoleVerb.List, RoleVerb.Watch],
248248
ApiGroups = [""],
249249
Resources = ["pods", "services"]
250250
},
251251
new ClusterRoleRule
252252
{
253-
Verbs = [ClusterRoleVerb.Get, ClusterRoleVerb.List],
253+
Verbs = [RoleVerb.Get, RoleVerb.List],
254254
ApiGroups = ["apps"],
255255
Resources = ["deployments", "replicasets"],
256256
ResourceNames = ["my-deployment"]
257257
},
258258
new ClusterRoleRule
259259
{
260-
Verbs = [ClusterRoleVerb.Get],
260+
Verbs = [RoleVerb.Get],
261261
NonResourceURLs = ["/api/*", "/metrics"]
262262
}
263263
]
Lines changed: 158 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using k8s.Models;
1+
using DevantlerTech.KubernetesGenerator.Core;
2+
using DevantlerTech.KubernetesGenerator.Native.Models;
23

34
namespace DevantlerTech.KubernetesGenerator.Native.Tests.RoleGeneratorTests;
45

@@ -9,32 +10,27 @@ namespace DevantlerTech.KubernetesGenerator.Native.Tests.RoleGeneratorTests;
910
public sealed class GenerateAsyncTests
1011
{
1112
/// <summary>
12-
/// Verifies the generated Role object.
13+
/// Verifies the generated Role object with basic permissions.
1314
/// </summary>
1415
/// <returns></returns>
1516
[Fact]
16-
public async Task GenerateAsync_WithAllPropertiesSet_ShouldGenerateAValidRole()
17+
public async Task GenerateAsync_WithBasicRole_ShouldGenerateAValidRole()
1718
{
1819
// Arrange
1920
var generator = new RoleGenerator();
20-
var model = new V1Role
21+
var model = new Role
2122
{
22-
ApiVersion = "rbac.authorization.k8s.io/v1",
23-
Kind = "Role",
24-
Metadata = new V1ObjectMeta
23+
Metadata = new Metadata
2524
{
26-
Name = "role",
27-
NamespaceProperty = "default"
25+
Name = "pod-reader",
26+
Namespace = "default"
2827
},
2928
Rules =
3029
[
31-
new V1PolicyRule
30+
new RoleRule
3231
{
33-
ApiGroups = ["api-group"],
34-
NonResourceURLs = ["url"],
35-
ResourceNames = ["resource-name"],
36-
Resources = ["resource"],
37-
Verbs = ["verb"]
32+
Verbs = [RoleVerb.Get, RoleVerb.List, RoleVerb.Watch],
33+
Resources = ["pods"]
3834
}
3935
]
4036
};
@@ -53,4 +49,151 @@ public async Task GenerateAsync_WithAllPropertiesSet_ShouldGenerateAValidRole()
5349
// Cleanup
5450
File.Delete(outputPath);
5551
}
52+
53+
/// <summary>
54+
/// Verifies the generated Role object with complex permissions including API groups and resource names.
55+
/// </summary>
56+
/// <returns></returns>
57+
[Fact]
58+
public async Task GenerateAsync_WithComplexRole_ShouldGenerateAValidRole()
59+
{
60+
// Arrange
61+
var generator = new RoleGenerator();
62+
var model = new Role
63+
{
64+
Metadata = new Metadata
65+
{
66+
Name = "complex-role",
67+
Namespace = "test-namespace"
68+
},
69+
Rules =
70+
[
71+
new RoleRule
72+
{
73+
Verbs = [RoleVerb.Get, RoleVerb.Create],
74+
Resources = ["pods", "services"],
75+
ApiGroups = ["", "apps"],
76+
ResourceNames = ["my-pod", "my-service"]
77+
}
78+
]
79+
};
80+
81+
// Act
82+
string fileName = "complex-role.yaml";
83+
string outputPath = Path.Combine(Path.GetTempPath(), fileName);
84+
if (File.Exists(outputPath))
85+
File.Delete(outputPath);
86+
await generator.GenerateAsync(model, outputPath);
87+
string fileContent = await File.ReadAllTextAsync(outputPath);
88+
89+
// Assert
90+
_ = await Verify(fileContent, extension: "yaml").UseFileName(fileName);
91+
92+
// Cleanup
93+
File.Delete(outputPath);
94+
}
95+
96+
/// <summary>
97+
/// Verifies that a <see cref="KubernetesGeneratorException"/> is thrown when the model does not have a name set.
98+
/// </summary>
99+
[Fact]
100+
public async Task GenerateAsync_WithoutName_ShouldThrowKubernetesGeneratorException()
101+
{
102+
// Arrange
103+
var generator = new RoleGenerator();
104+
var model = new Role
105+
{
106+
Metadata = new Metadata
107+
{
108+
Name = "" // Empty name should trigger validation
109+
},
110+
Rules =
111+
[
112+
new RoleRule
113+
{
114+
Verbs = [RoleVerb.Get],
115+
Resources = ["pods"]
116+
}
117+
]
118+
};
119+
120+
// Act & Assert
121+
_ = await Assert.ThrowsAsync<KubernetesGeneratorException>(() => generator.GenerateAsync(model, Path.GetTempFileName()));
122+
}
123+
124+
/// <summary>
125+
/// Verifies that a <see cref="KubernetesGeneratorException"/> is thrown when the model does not have any rules.
126+
/// </summary>
127+
[Fact]
128+
public async Task GenerateAsync_WithoutRules_ShouldThrowKubernetesGeneratorException()
129+
{
130+
// Arrange
131+
var generator = new RoleGenerator();
132+
var model = new Role
133+
{
134+
Metadata = new Metadata
135+
{
136+
Name = "test-role"
137+
}
138+
};
139+
140+
// Act & Assert
141+
_ = await Assert.ThrowsAsync<KubernetesGeneratorException>(() => generator.GenerateAsync(model, Path.GetTempFileName()));
142+
}
143+
144+
/// <summary>
145+
/// Verifies that a <see cref="KubernetesGeneratorException"/> is thrown when a rule has no verbs.
146+
/// </summary>
147+
[Fact]
148+
public async Task GenerateAsync_WithRuleWithoutVerbs_ShouldThrowKubernetesGeneratorException()
149+
{
150+
// Arrange
151+
var generator = new RoleGenerator();
152+
var model = new Role
153+
{
154+
Metadata = new Metadata
155+
{
156+
Name = "test-role"
157+
},
158+
Rules =
159+
[
160+
new RoleRule
161+
{
162+
Verbs = [], // Empty verbs should trigger validation
163+
Resources = ["pods"]
164+
}
165+
]
166+
};
167+
168+
// Act & Assert
169+
_ = await Assert.ThrowsAsync<KubernetesGeneratorException>(() => generator.GenerateAsync(model, Path.GetTempFileName()));
170+
}
171+
172+
/// <summary>
173+
/// Verifies that a <see cref="KubernetesGeneratorException"/> is thrown when a rule has no resources.
174+
/// </summary>
175+
[Fact]
176+
public async Task GenerateAsync_WithRuleWithoutResources_ShouldThrowKubernetesGeneratorException()
177+
{
178+
// Arrange
179+
var generator = new RoleGenerator();
180+
var model = new Role
181+
{
182+
Metadata = new Metadata
183+
{
184+
Name = "test-role"
185+
},
186+
Rules =
187+
[
188+
new RoleRule
189+
{
190+
Verbs = [RoleVerb.Get]
191+
// Resources missing - should trigger validation
192+
}
193+
]
194+
};
195+
196+
// Act & Assert
197+
_ = await Assert.ThrowsAsync<KubernetesGeneratorException>(() => generator.GenerateAsync(model, Path.GetTempFileName()));
198+
}
56199
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
apiVersion: rbac.authorization.k8s.io/v1
3+
kind: Role
4+
metadata:
5+
namespace: test-namespace
6+
name: complex-role
7+
rules:
8+
- verbs:
9+
- get
10+
- create
11+
apiGroups:
12+
- ''
13+
- apps
14+
resources:
15+
- pods
16+
- services
17+
resourceNames:
18+
- my-pod
19+
- my-service

0 commit comments

Comments
 (0)