generated from vshn/go-bootstrap
/
groupsync_controller.go
169 lines (141 loc) · 5.48 KB
/
groupsync_controller.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package controllers
import (
"context"
"fmt"
"slices"
"strings"
controlv1 "github.com/appuio/control-api/apis/v1"
userv1 "github.com/openshift/api/user/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/appuio/appuio-cloud-agent/controllers/clustersource"
)
// GroupSyncReconciler reconciles a Group object
type GroupSyncReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
ForeignClient client.Client
ControlAPIFinalizerZoneName string
}
// OrganizationMembersManifestName is the static name of the OrganizationMembers manifest
// in the control-api cluster.
const OrganizationMembersManifestName = "members"
const UpstreamFinalizerPrefix = "agent.appuio.io/group-zone-"
//+kubebuilder:rbac:groups=user.openshift.io,resources=groups,verbs=get;list;watch;update;patch;create;delete
// Reconcile syncs the Group with the upstream OrganizationMembers or Team resource from the foreign (Control-API) cluster.
func (r *GroupSyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
l := log.FromContext(ctx)
l.Info("Reconciling Group")
finalizerName := UpstreamFinalizerPrefix + r.ControlAPIFinalizerZoneName
var members []controlv1.UserRef
var upstream client.Object
isTeam := strings.ContainsRune(req.Name, '+')
if isTeam {
nsn := strings.SplitN(req.Name, "+", 2)
ns, name := nsn[0], nsn[1]
var u controlv1.Team
if err := r.ForeignClient.Get(ctx, client.ObjectKey{Namespace: ns, Name: name}, &u); err != nil {
if apierrors.IsNotFound(err) {
l.Info("Upstream team not found")
return ctrl.Result{}, nil
}
l.Error(err, "unable to get upstream Team")
return ctrl.Result{}, err
}
upstream = &u
members = u.Status.ResolvedUserRefs
} else {
var u controlv1.OrganizationMembers
if err := r.ForeignClient.Get(ctx, client.ObjectKey{Namespace: req.Name, Name: OrganizationMembersManifestName}, &u); err != nil {
if apierrors.IsNotFound(err) {
l.Info("Upstream organization members not found")
return ctrl.Result{}, nil
}
l.Error(err, "unable to get upstream OrganizationMembers")
return ctrl.Result{}, err
}
upstream = &u
members = u.Status.ResolvedUserRefs
}
group := &userv1.Group{ObjectMeta: metav1.ObjectMeta{Name: req.Name}}
if upstream.GetDeletionTimestamp() != nil {
l.Info("Upstream Group is being deleted")
err := r.Delete(ctx, group)
if err != nil && !apierrors.IsNotFound(err) {
l.Error(err, "unable to delete Group")
return ctrl.Result{}, err
}
l.Info("Group deleted")
if controllerutil.RemoveFinalizer(upstream, finalizerName) {
if err := r.ForeignClient.Update(ctx, upstream); err != nil {
l.Error(err, "unable to remove finalizer from upstream")
return ctrl.Result{}, err
}
}
l.Info("Finalizer removed from upstream", "finalizer", finalizerName)
return ctrl.Result{}, nil
}
op, err := controllerutil.CreateOrUpdate(ctx, r.Client, group, func() error {
group.Users = make([]string, len(members))
for i, member := range members {
group.Users[i] = member.Name
}
slices.Sort(group.Users)
return nil
})
if err != nil {
l.Error(err, "unable to create or update (%q) Group", op)
return ctrl.Result{}, err
}
l.Info("Group reconciled", "operation", op)
if controllerutil.AddFinalizer(upstream, finalizerName) {
if err := r.ForeignClient.Update(ctx, upstream); err != nil {
l.Error(err, "unable to add finalizer to upstream")
return ctrl.Result{}, err
}
l.Info("Finalizer added to upstream", "finalizer", finalizerName)
}
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *GroupSyncReconciler) SetupWithManagerAndForeignCluster(mgr ctrl.Manager, foreign clustersource.ClusterSource) error {
return ctrl.NewControllerManagedBy(mgr).
For(&userv1.Group{}).
WatchesRawSource(foreign.SourceFor(&controlv1.Team{}), handler.EnqueueRequestsFromMapFunc(teamMapper)).
WatchesRawSource(foreign.SourceFor(&controlv1.OrganizationMembers{}), handler.EnqueueRequestsFromMapFunc(organizationMembersMapper)).
Complete(r)
}
// teamMapper maps the combination of namespace and name of the manifest as the group name to reconcile.
// The namespace is the organization for the teams.
func teamMapper(ctx context.Context, o client.Object) []reconcile.Request {
team, ok := o.(*controlv1.Team)
if !ok {
log.FromContext(ctx).Error(nil, "expected a Team object got a %T", o)
return []reconcile.Request{}
}
return []reconcile.Request{
{NamespacedName: types.NamespacedName{Name: fmt.Sprintf("%s+%s", team.Namespace, team.Name)}},
}
}
// organizationMembersMapper maps the namespace of the manifest as the group name to reconcile.
// The name is static and the organization is in the namespace field.
func organizationMembersMapper(ctx context.Context, o client.Object) []reconcile.Request {
member, ok := o.(*controlv1.OrganizationMembers)
if !ok {
log.FromContext(ctx).Error(nil, "expected a OrganizationMembers object got a %T", o)
return []reconcile.Request{}
}
return []reconcile.Request{
{NamespacedName: types.NamespacedName{Name: member.Namespace}},
}
}