Skip to content

Commit 5fa3534

Browse files
Add ServiceAccount generation from WSA list
1 parent 01e00bf commit 5fa3534

File tree

8 files changed

+528
-80
lines changed

8 files changed

+528
-80
lines changed

internal/controller/workloadserviceaccount_controller.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,12 @@ func (r *WorkloadServiceAccountReconciler) Reconcile(ctx context.Context, req ct
5454

5555
log.Info("Found WSAs in namespace", "count", len(wsaList.Items))
5656

57-
for _, currentWSA := range wsaList.Items {
58-
for _, project := range currentWSA.Spec.Scope.Projects {
59-
log.Info("WSA has project scope", "wsa", currentWSA.Name, "project", project)
60-
}
61-
for _, environment := range currentWSA.Spec.Scope.Environments {
62-
log.Info("WSA has environment scope", "wsa", currentWSA.Name, "environment", environment)
63-
}
64-
for _, tenant := range currentWSA.Spec.Scope.Tenants {
65-
log.Info("WSA has tenant scope", "wsa", currentWSA.Name, "tenant", tenant)
66-
}
67-
for _, step := range currentWSA.Spec.Scope.Steps {
68-
log.Info("WSA has step scope", "wsa", currentWSA.Name, "step", step)
69-
}
57+
if err := r.Engine.RegenerateFromWSAs(wsaList.Items); err != nil {
58+
log.Error(err, "failed to regenerate ServiceAccount mappings from WSAs")
59+
return ctrl.Result{}, err
7060
}
7161

72-
log.Info("Successfully reconciled WorkloadServiceAccounts")
62+
log.Info("Successfully reconciled all WorkloadServiceAccounts")
7363
return ctrl.Result{}, nil
7464
}
7565

internal/controller/workloadserviceaccount_controller_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2929

3030
agentoctopuscomv1beta1 "github.com/octopusdeploy/octopus-permissions-controller/api/v1beta1"
31+
"github.com/octopusdeploy/octopus-permissions-controller/internal/rules"
3132
)
3233

3334
var _ = Describe("WorkloadServiceAccount Controller", func() {
@@ -68,9 +69,11 @@ var _ = Describe("WorkloadServiceAccount Controller", func() {
6869
})
6970
It("should successfully reconcile the resource", func() {
7071
By("Reconciling the created resource")
72+
engine := rules.NewInMemoryEngine()
7173
controllerReconciler := &WorkloadServiceAccountReconciler{
7274
Client: k8sClient,
7375
Scheme: k8sClient.Scheme(),
76+
Engine: &engine,
7477
}
7578

7679
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{

internal/rules/engine.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package rules
22

33
import (
4+
"fmt"
5+
46
"github.com/octopusdeploy/octopus-permissions-controller/api/v1beta1"
57
)
68

@@ -17,14 +19,21 @@ type Scope struct {
1719
Step string `json:"step"`
1820
}
1921

22+
func (s Scope) String() string {
23+
return fmt.Sprintf("projects=%s,environments=%s,tenants=%s,steps=%s",
24+
s.Project,
25+
s.Environment,
26+
s.Tenant,
27+
s.Step)
28+
}
29+
2030
type Rule struct {
2131
Permissions v1beta1.WorkloadServiceAccountPermissions `json:"permissions"`
2232
}
2333

2434
type Engine interface {
2535
GetServiceAccountForScope(scope Scope, agentName AgentName) (ServiceAccountName, error)
26-
AddScopeRuleset(scope Scope, rule Rule, targetNamespace Namespace) error
27-
RemoveScopeRuleset(scope Scope, rule Rule, targetNamespace Namespace) error
36+
RegenerateFromWSAs(wsas []v1beta1.WorkloadServiceAccount) error
2837
}
2938

3039
type InMemoryEngine struct {
@@ -38,7 +47,7 @@ func NewInMemoryEngine() InMemoryEngine {
3847
}
3948
}
4049

41-
func (i InMemoryEngine) GetServiceAccountForScope(scope Scope, agentName AgentName) (ServiceAccountName, error) {
50+
func (i *InMemoryEngine) GetServiceAccountForScope(scope Scope, agentName AgentName) (ServiceAccountName, error) {
4251
if agentRules, ok := i.rules[agentName]; ok {
4352
if sa, ok := agentRules[scope]; ok {
4453
return sa, nil
@@ -47,12 +56,21 @@ func (i InMemoryEngine) GetServiceAccountForScope(scope Scope, agentName AgentNa
4756
return "", nil
4857
}
4958

50-
func (i InMemoryEngine) AddScopeRuleset(scope Scope, rule Rule, targetNamespace Namespace) error {
51-
// TODO: Implement me
52-
return nil
53-
}
59+
func (i *InMemoryEngine) RegenerateFromWSAs(wsas []v1beta1.WorkloadServiceAccount) error {
60+
// TODO: Support scoping WSAs to specific agents
61+
const defaultAgent = AgentName("default")
62+
63+
scopePermissionsMap := GenerateAllScopesWithPermissions(wsas)
64+
// TODO: Optimize by comparing with existing rules and only updating changed ones
65+
66+
i.rules[defaultAgent] = make(map[Scope]ServiceAccountName)
67+
68+
for scope := range scopePermissionsMap {
69+
serviceAccountName := GenerateServiceAccountName(scope)
70+
i.rules[defaultAgent][scope] = serviceAccountName
71+
}
72+
73+
// TODO: Create or update Kubernetes resources (ServiceAccounts, Roles, RoleBindings) based on the generated rules
5474

55-
func (i InMemoryEngine) RemoveScopeRuleset(scope Scope, rule Rule, targetNamespace Namespace) error {
56-
// TODO: Implement me
5775
return nil
5876
}

internal/rules/generator.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package rules
2+
3+
import (
4+
"github.com/octopusdeploy/octopus-permissions-controller/api/v1beta1"
5+
)
6+
7+
// GenerateAllScopesWithPermissions creates ServiceAccounts for all unique scope combinations
8+
func GenerateAllScopesWithPermissions(wsas []v1beta1.WorkloadServiceAccount) map[Scope]v1beta1.WorkloadServiceAccountPermissions {
9+
if len(wsas) == 0 {
10+
return nil
11+
}
12+
13+
result := make(map[Scope]v1beta1.WorkloadServiceAccountPermissions)
14+
15+
// Get all unique scope values (including wildcards)
16+
projectSet, environmentSet, tenantSet, stepSet := getAllScopeValues(wsas)
17+
18+
// Generate all possible scope combinations
19+
allScopeCombinations := generateAllScopeCombinations(projectSet, environmentSet, tenantSet, stepSet)
20+
21+
// For each scope combination find WSAs that match the scope and merge their permissions
22+
// If no WSAs match the scope, no ServiceAccount is created for that scope
23+
for _, scope := range allScopeCombinations {
24+
matchingWSAs := getMatchingWSAsForScope(scope, wsas)
25+
if len(matchingWSAs) > 0 {
26+
var mergedPermissions v1beta1.WorkloadServiceAccountPermissions
27+
for _, wsa := range matchingWSAs {
28+
mergedPermissions = mergePermissions(mergedPermissions, wsa.Spec.Permissions)
29+
}
30+
result[scope] = mergedPermissions
31+
}
32+
}
33+
34+
return result
35+
}
36+
37+
// getAllScopeValues extracts all unique scope values from all WSAs
38+
func getAllScopeValues(wsas []v1beta1.WorkloadServiceAccount) (projects, environments, tenants, steps map[string]struct{}) {
39+
projects = make(map[string]struct{})
40+
environments = make(map[string]struct{})
41+
tenants = make(map[string]struct{})
42+
steps = make(map[string]struct{})
43+
44+
hasWildcardProjects := false
45+
hasWildcardEnvironments := false
46+
hasWildcardTenants := false
47+
hasWildcardSteps := false
48+
49+
for _, wsa := range wsas {
50+
processScopeValues(wsa.Spec.Scope.Projects, projects, &hasWildcardProjects)
51+
processScopeValues(wsa.Spec.Scope.Environments, environments, &hasWildcardEnvironments)
52+
processScopeValues(wsa.Spec.Scope.Tenants, tenants, &hasWildcardTenants)
53+
processScopeValues(wsa.Spec.Scope.Steps, steps, &hasWildcardSteps)
54+
}
55+
56+
if hasWildcardProjects {
57+
projects["*"] = struct{}{}
58+
}
59+
if hasWildcardEnvironments {
60+
environments["*"] = struct{}{}
61+
}
62+
if hasWildcardTenants {
63+
tenants["*"] = struct{}{}
64+
}
65+
if hasWildcardSteps {
66+
steps["*"] = struct{}{}
67+
}
68+
69+
return projects, environments, tenants, steps
70+
}
71+
72+
func processScopeValues(slice []string, valueSet map[string]struct{}, hasWildcard *bool) {
73+
if !*hasWildcard && len(slice) == 0 {
74+
*hasWildcard = true
75+
} else {
76+
for _, value := range slice {
77+
valueSet[value] = struct{}{}
78+
}
79+
}
80+
}
81+
82+
// generateAllScopeCombinations generates all possible scope combinations
83+
func generateAllScopeCombinations(projects, environments, tenants, steps map[string]struct{}) []Scope {
84+
capacity := len(projects) * len(environments) * len(tenants) * len(steps)
85+
scopes := make([]Scope, 0, capacity)
86+
87+
for project := range projects {
88+
for environment := range environments {
89+
for tenant := range tenants {
90+
for step := range steps {
91+
scope := Scope{
92+
Project: project,
93+
Environment: environment,
94+
Tenant: tenant,
95+
Step: step,
96+
}
97+
scopes = append(scopes, scope)
98+
}
99+
}
100+
}
101+
}
102+
103+
return scopes
104+
}
105+
106+
// getMatchingWSAsForScope returns all WSAs that apply to the given concrete scope
107+
func getMatchingWSAsForScope(scope Scope, wsas []v1beta1.WorkloadServiceAccount) []v1beta1.WorkloadServiceAccount {
108+
var matchingWSAs []v1beta1.WorkloadServiceAccount
109+
110+
for _, wsa := range wsas {
111+
if scopeMatchesWSA(scope, wsa) {
112+
matchingWSAs = append(matchingWSAs, wsa)
113+
}
114+
}
115+
116+
return matchingWSAs
117+
}
118+
119+
// scopeMatchesWSA checks if a WSA applies to a concrete scope
120+
func scopeMatchesWSA(scope Scope, wsa v1beta1.WorkloadServiceAccount) bool {
121+
return hasMatchingScopeValue(wsa.Spec.Scope.Projects, scope.Project) &&
122+
hasMatchingScopeValue(wsa.Spec.Scope.Environments, scope.Environment) &&
123+
hasMatchingScopeValue(wsa.Spec.Scope.Tenants, scope.Tenant) &&
124+
hasMatchingScopeValue(wsa.Spec.Scope.Steps, scope.Step)
125+
}
126+
127+
// hasMatchingScopeValue checks if a WSA dimension matches a concrete scope value
128+
func hasMatchingScopeValue(wsaScopes []string, scopeValue string) bool {
129+
// Empty WSA scope list means wildcard (matches any value)
130+
if len(wsaScopes) == 0 {
131+
return true
132+
}
133+
134+
for _, value := range wsaScopes {
135+
if value == scopeValue {
136+
return true
137+
}
138+
}
139+
140+
return false
141+
}
142+
143+
// mergePermissions combines permissions from multiple WSAs for the same scope
144+
func mergePermissions(existing, new v1beta1.WorkloadServiceAccountPermissions) v1beta1.WorkloadServiceAccountPermissions {
145+
merged := v1beta1.WorkloadServiceAccountPermissions{
146+
ClusterRoles: append(existing.ClusterRoles, new.ClusterRoles...),
147+
Roles: append(existing.Roles, new.Roles...),
148+
Permissions: append(existing.Permissions, new.Permissions...),
149+
}
150+
151+
// TODO: Deduplicate identical roles and permissions
152+
153+
return merged
154+
}

0 commit comments

Comments
 (0)