Skip to content

Commit

Permalink
Teams Adapter (#12)
Browse files Browse the repository at this point in the history
Implement teams reconciliation and sync
  • Loading branch information
bastjan authored Mar 2, 2022
1 parent de60ea8 commit 8f6e11b
Show file tree
Hide file tree
Showing 19 changed files with 803 additions and 173 deletions.
23 changes: 23 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ rules:
- get
- patch
- update
- apiGroups:
- appuio.io
resources:
- teams
verbs:
- get
- patch
- update
- watch
- apiGroups:
- appuio.io
resources:
- teams/finalizers
verbs:
- update
- apiGroups:
- appuio.io
resources:
- teams/status
verbs:
- get
- patch
- update
- apiGroups:
- organization.appuio.io
resources:
Expand Down
13 changes: 9 additions & 4 deletions controllers/ZZ_mock_keycloak_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 2 additions & 8 deletions controllers/organization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ type OrganizationReconciler struct {
Scheme *runtime.Scheme

Keycloak KeycloakClient

// ClusteRoles to give to group members when importing
SyncClusterRoles []string
}

//go:generate go run github.com/golang/mock/mockgen -destination=./ZZ_mock_eventrecorder_test.go -package controllers_test k8s.io/client-go/tools/record EventRecorder
Expand All @@ -35,7 +32,7 @@ type OrganizationReconciler struct {
// KeycloakClient is an abstraction to interact with the Keycloak API
type KeycloakClient interface {
PutGroup(ctx context.Context, group keycloak.Group) (keycloak.Group, error)
DeleteGroup(ctx context.Context, groupName string) error
DeleteGroup(ctx context.Context, path ...string) error
ListGroups(ctx context.Context) ([]keycloak.Group, error)
}

Expand Down Expand Up @@ -166,10 +163,7 @@ func buildKeycloakGroup(org *orgv1.Organization, memb *controlv1.OrganizationMem
groupMem = append(groupMem, u.Name)
}

return keycloak.Group{
Name: org.Name,
Members: groupMem,
}
return keycloak.NewGroup(org.Name).WithMembers(groupMem...)
}

// SetupWithManager sets up the controller with the Manager.
Expand Down
18 changes: 9 additions & 9 deletions controllers/organization_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import (
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
)

func Test_Reconcile_Success(t *testing.T) {
func Test_OrganizationController_Reconcile_Success(t *testing.T) {
ctx := context.Background()

c, keyMock, _ := prepareTest(t, fooOrg, fooMemb)
group := keycloak.Group{Name: "foo", Members: []string{"bar", "bar3"}}
group := keycloak.NewGroup("foo").WithMembers("bar", "bar3")
keyMock.EXPECT().
PutGroup(gomock.Any(), group).
Return(group, nil).
Expand All @@ -55,11 +55,11 @@ func Test_Reconcile_Success(t *testing.T) {
assert.Equal(t, "keycloak-adapter.vshn.net/finalizer", newMemb.Finalizers[0], "expected finalizer")
}

func Test_Reconcile_Failure(t *testing.T) {
func Test_OrganizationController_Reconcile_Failure(t *testing.T) {
ctx := context.Background()

c, keyMock, erMock := prepareTest(t, fooOrg, fooMemb)
group := keycloak.Group{Name: "foo", Members: []string{"bar", "bar3"}}
group := keycloak.NewGroup("foo").WithMembers("bar", "bar3")
keyMock.EXPECT().
PutGroup(gomock.Any(), group).
Return(keycloak.Group{}, errors.New("create failed")).
Expand Down Expand Up @@ -91,11 +91,11 @@ func Test_Reconcile_Failure(t *testing.T) {
assert.Equal(t, "keycloak-adapter.vshn.net/finalizer", newMemb.Finalizers[0], "expected finalizer")
}

func Test_Reconcile_Member_Failure(t *testing.T) {
func Test_OrganizationController_Reconcile_Member_Failure(t *testing.T) {
ctx := context.Background()

c, keyMock, erMock := prepareTest(t, fooOrg, fooMemb)
group := keycloak.Group{Name: "foo", Members: []string{"bar", "bar3"}}
group := keycloak.NewGroup("foo").WithMembers("bar", "bar3")
keyMock.EXPECT().
PutGroup(gomock.Any(), group).
Return(keycloak.Group{}, &keycloak.MembershipSyncErrors{
Expand Down Expand Up @@ -141,7 +141,7 @@ func Test_Reconcile_Member_Failure(t *testing.T) {
assert.Equal(t, "keycloak-adapter.vshn.net/finalizer", newMemb.Finalizers[0], "expected finalizer")
}

func Test_Reconcile_Delete(t *testing.T) {
func Test_OrganizationController_Reconcile_Delete(t *testing.T) {
ctx := context.Background()

org := *fooOrg
Expand Down Expand Up @@ -170,7 +170,7 @@ func Test_Reconcile_Delete(t *testing.T) {
require.Error(t, c.Get(ctx, types.NamespacedName{Name: "foo"}, &newOrg))
}

func Test_Reconcile_Delete_Failure(t *testing.T) {
func Test_OrganizationController_Reconcile_Delete_Failure(t *testing.T) {
ctx := context.Background()

org := *fooOrg
Expand Down Expand Up @@ -205,7 +205,7 @@ func Test_Reconcile_Delete_Failure(t *testing.T) {
}

// Reconcile should ignore organizations that are being imported
func Test_Reconcile_Ignore(t *testing.T) {
func Test_OrganizationController_Reconcile_Ignore(t *testing.T) {
ctx := context.Background()

org := *fooOrg
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,43 @@ import (
rbacv1 "k8s.io/api/rbac/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"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)

var orgImportAnnot = "keycloak-adapter.vshn.net/importing"
const orgImportAnnot = "keycloak-adapter.vshn.net/importing"

// PeriodicSyncer reconciles a Organization object
type PeriodicSyncer struct {
client.Client
Recorder record.EventRecorder

Keycloak KeycloakClient

// SyncClusterRoles to give to group members when importing
SyncClusterRoles []string
}

// Sync lists all Keycloak groups in the realm and creates corresponding Organizations if they do not exist
func (r *OrganizationReconciler) Sync(ctx context.Context) error {
func (r *PeriodicSyncer) Sync(ctx context.Context) error {
logger := log.FromContext(ctx)

gs, err := r.Keycloak.ListGroups(ctx)
if err != nil {
return fmt.Errorf("cannot list Keycloak groups: %w", err)
}

orgMap, err := r.fetchOrganiztionMap(ctx)
orgMap, err := r.fetchOrganizationMap(ctx)
if err != nil {
return fmt.Errorf("cannot list Organizations: %w", err)
}

var groupErr error
for _, g := range gs {
org, err := r.syncGroup(ctx, g, orgMap[g.Name])
org, err := r.syncGroup(ctx, g, orgMap)
if err != nil {
if groupErr == nil {
groupErr = errors.New("")
Expand All @@ -43,7 +57,7 @@ func (r *OrganizationReconciler) Sync(ctx context.Context) error {
if org != nil {
r.Recorder.Event(org, "Warning", "ImportFailed", err.Error())
}
groupErr = fmt.Errorf("%w\n%s: %s", groupErr, g.Name, err.Error())
groupErr = fmt.Errorf("%w\n%s: %s", groupErr, g.BaseName(), err.Error())
}
}
if groupErr != nil {
Expand All @@ -52,7 +66,45 @@ func (r *OrganizationReconciler) Sync(ctx context.Context) error {
return nil
}

func (r *OrganizationReconciler) syncGroup(ctx context.Context, g keycloak.Group, org *orgv1.Organization) (*orgv1.Organization, error) {
func (r *PeriodicSyncer) syncGroup(ctx context.Context, g keycloak.Group, orgMap map[string]*orgv1.Organization) (runtime.Object, error) {
logger := log.FromContext(ctx)

const depth = 0
switch len(g.PathMembers()) - depth {
case 1:
return r.syncOrganization(ctx, g, orgMap[g.BaseName()])
case 2:
return r.syncTeam(ctx, g)
}

logger.Info("skipped syncing group. invalid hierarchy", "group", g)
return nil, nil
}

func (r *PeriodicSyncer) syncTeam(ctx context.Context, g keycloak.Group) (*controlv1.Team, error) {
logger := log.FromContext(ctx)
var err error

path := g.PathMembers()
teamKey := types.NamespacedName{Namespace: path[len(path)-2], Name: path[len(path)-1]}

team := &controlv1.Team{}
err = r.Client.Get(ctx, teamKey, team)
if err != nil && apierrors.IsNotFound(err) {
logger.V(1).WithValues("group", g).Info("creating team")
t, err := r.createTeam(ctx, teamKey.Namespace, teamKey.Name, g.Members)
if err != nil {
return nil, fmt.Errorf("error creating team %+v: %w", teamKey, err)
}
team = t
} else if err != nil {
return nil, fmt.Errorf("error getting team %+v: %w", teamKey, err)
}

return team, nil
}

func (r *PeriodicSyncer) syncOrganization(ctx context.Context, g keycloak.Group, org *orgv1.Organization) (*orgv1.Organization, error) {
logger := log.FromContext(ctx)
var err error

Expand Down Expand Up @@ -81,7 +133,7 @@ func (r *OrganizationReconciler) syncGroup(ctx context.Context, g keycloak.Group
return org, err
}

func (r *OrganizationReconciler) fetchOrganiztionMap(ctx context.Context) (map[string]*orgv1.Organization, error) {
func (r *PeriodicSyncer) fetchOrganizationMap(ctx context.Context) (map[string]*orgv1.Organization, error) {
orgs := orgv1.OrganizationList{}
err := r.List(ctx, &orgs)
if err != nil {
Expand All @@ -95,32 +147,51 @@ func (r *OrganizationReconciler) fetchOrganiztionMap(ctx context.Context) (map[s
return orgMap, nil
}

func (r *OrganizationReconciler) startImportOrganizationFromGroup(ctx context.Context, group keycloak.Group) (*orgv1.Organization, error) {
func (r *PeriodicSyncer) createTeam(ctx context.Context, namespace, name string, members []string) (*controlv1.Team, error) {
team := &controlv1.Team{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: controlv1.TeamSpec{
DisplayName: name,
},
}

team.Spec.UserRefs = make([]controlv1.UserRef, len(members))
for i, m := range members {
team.Spec.UserRefs[i] = controlv1.UserRef{Name: m}
}
err := r.Create(ctx, team)
return team, err
}

func (r *PeriodicSyncer) startImportOrganizationFromGroup(ctx context.Context, group keycloak.Group) (*orgv1.Organization, error) {
org := &orgv1.Organization{
ObjectMeta: metav1.ObjectMeta{
Name: group.Name,
Name: group.BaseName(),
Annotations: map[string]string{
orgImportAnnot: "true",
},
},
Spec: orgv1.OrganizationSpec{
DisplayName: group.Name,
DisplayName: group.BaseName(),
},
}
err := r.Create(ctx, org)
return org, err
}

func (r *OrganizationReconciler) finishImportOrganizationFromGroup(ctx context.Context, org *orgv1.Organization) error {
func (r *PeriodicSyncer) finishImportOrganizationFromGroup(ctx context.Context, org *orgv1.Organization) error {
delete(org.Annotations, orgImportAnnot)
return r.Update(ctx, org)

}

func (r *OrganizationReconciler) updateOrganizationMembersFromGroup(ctx context.Context, group keycloak.Group) error {
func (r *PeriodicSyncer) updateOrganizationMembersFromGroup(ctx context.Context, group keycloak.Group) error {
orgMemb := controlv1.OrganizationMembers{}
err := r.Get(ctx, types.NamespacedName{
Namespace: group.Name,
Namespace: group.BaseName(),
Name: "members",
}, &orgMemb)
if err != nil {
Expand All @@ -133,7 +204,7 @@ func (r *OrganizationReconciler) updateOrganizationMembersFromGroup(ctx context.
return r.Update(ctx, &orgMemb)
}

func (r *OrganizationReconciler) setRolebindingsFromGroup(ctx context.Context, group keycloak.Group) error {
func (r *PeriodicSyncer) setRolebindingsFromGroup(ctx context.Context, group keycloak.Group) error {
subjects := []rbacv1.Subject{}
for _, m := range group.Members {
subjects = append(subjects, rbacv1.Subject{
Expand All @@ -146,14 +217,14 @@ func (r *OrganizationReconciler) setRolebindingsFromGroup(ctx context.Context, g
for _, rbName := range r.SyncClusterRoles {

rb := rbacv1.RoleBinding{}
err := r.Get(ctx, types.NamespacedName{Namespace: group.Name, Name: rbName}, &rb)
err := r.Get(ctx, types.NamespacedName{Namespace: group.BaseName(), Name: rbName}, &rb)
if err != nil && !apierrors.IsNotFound(err) {
return err
}
if apierrors.IsNotFound(err) {
rb := rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Namespace: group.Name,
Namespace: group.BaseName(),
Name: rbName,
},
Subjects: subjects,
Expand Down
Loading

0 comments on commit 8f6e11b

Please sign in to comment.