Skip to content

Commit

Permalink
Merge pull request #203 from appuio/feat/default-org-reconcoler
Browse files Browse the repository at this point in the history
Add default organization to users that are in exactly one organization
  • Loading branch information
HappyTetrahedron committed Feb 12, 2024
2 parents a8e87c3 + eeafd34 commit e4164a1
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 0 deletions.
8 changes: 8 additions & 0 deletions controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,14 @@ func setupManager(
return nil, err
}
}
dor := &controllers.DefaultOrganizationReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("organization-members-controller"),
}
if err = dor.SetupWithManager(mgr); err != nil {
return nil, err
}
obenc := &controllers.OrgBillingEntityNameCacheController{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand Down
101 changes: 101 additions & 0 deletions controllers/default_organization_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package controllers

import (
"context"

"go.uber.org/multierr"
apierrors "k8s.io/apimachinery/pkg/api/errors"
v1 "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/log"

controlv1 "github.com/appuio/control-api/apis/v1"
)

// DefaultOrganizationReconciler reconciles User resources to ensure they have a DefaultOrganization set if applicable.
type DefaultOrganizationReconciler struct {
client.Client
Recorder record.EventRecorder
Scheme *runtime.Scheme
}

//+kubebuilder:rbac:groups=appuio.io,resources=organizationmembers,verbs=get;list;watch
//+kubebuilder:rbac:groups=appuio.io,resources=users,verbs=get;list;watch;update;patch
//+kubebuilder:rbac:groups=appuio.io,resources=users/status,verbs=get

// Reconcile reacts on changes of memberships and sets members' default organization if appropriate
func (r *DefaultOrganizationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
log.V(1).WithValues("request", req).Info("Reconciling")

memb := controlv1.OrganizationMembers{}
if err := r.Get(ctx, req.NamespacedName, &memb); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}

if !memb.ObjectMeta.DeletionTimestamp.IsZero() {
return ctrl.Result{}, nil
}

allMemberships := controlv1.OrganizationMembersList{}
if err := r.List(ctx, &allMemberships); err != nil {
return ctrl.Result{}, err
}

var errGroup error
for _, user := range memb.Status.ResolvedUserRefs {
myOrgs := make([]string, 0)

for _, membership := range allMemberships.Items {
for _, membershipUser := range membership.Status.ResolvedUserRefs {
if user.Name == membershipUser.Name {
myOrgs = append(myOrgs, membership.Namespace)
break
}
}
}
if len(myOrgs) == 1 {
err := setUserDefaultOrganization(ctx, r.Client, user.Name, myOrgs[0])
errGroup = multierr.Append(errGroup, err)
}
}

return ctrl.Result{}, errGroup
}

func setUserDefaultOrganization(ctx context.Context, c client.Client, userName string, orgName string) error {
user := controlv1.User{}
if err := c.Get(ctx, types.NamespacedName{Name: userName}, &user); err != nil {
if !apierrors.IsNotFound(err) {
return err
}
return c.Create(ctx, &controlv1.User{
ObjectMeta: v1.ObjectMeta{
Name: userName,
},
Spec: controlv1.UserSpec{
Preferences: controlv1.UserPreferences{
DefaultOrganizationRef: orgName,
},
},
})
}

if user.Spec.Preferences.DefaultOrganizationRef != "" {
return nil
}

user.Spec.Preferences.DefaultOrganizationRef = orgName
return c.Update(ctx, &user)
}

// SetupWithManager sets up the controller with the Manager.
func (r *DefaultOrganizationReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&controlv1.OrganizationMembers{}).
Complete(r)
}
225 changes: 225 additions & 0 deletions controllers/default_organization_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package controllers_test

import (
"context"
"errors"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

controlv1 "github.com/appuio/control-api/apis/v1"
. "github.com/appuio/control-api/controllers"
)

var testMemberships1 = controlv1.OrganizationMembers{
ObjectMeta: metav1.ObjectMeta{
Name: "members",
Namespace: "foo-gmbh",
},
Spec: controlv1.OrganizationMembersSpec{
UserRefs: []controlv1.UserRef{
{Name: "u1"},
{Name: "u2"},
{Name: "u3"},
},
},
Status: controlv1.OrganizationMembersStatus{
ResolvedUserRefs: []controlv1.UserRef{
{Name: "u1"},
{Name: "u2"},
{Name: "u3"},
},
},
}

var testMemberships2 = controlv1.OrganizationMembers{
ObjectMeta: metav1.ObjectMeta{
Name: "members",
Namespace: "bar-gmbh",
},
Spec: controlv1.OrganizationMembersSpec{
UserRefs: []controlv1.UserRef{
{Name: "u1"},
},
},
Status: controlv1.OrganizationMembersStatus{
ResolvedUserRefs: []controlv1.UserRef{
{Name: "u1"},
},
},
}

var u1 = controlv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "u1",
},
}
var u2 = controlv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "u2",
},
}
var u3 = controlv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "u3",
},
}

func Test_DefaultOrganizationReconciler_Reconcile_Success(t *testing.T) {
ctx := context.Background()
c := prepareTest(t, &testMemberships1, &testMemberships2, &u1, &u2, &u3)
fakeRecorder := record.NewFakeRecorder(3)

_, err := (&DefaultOrganizationReconciler{
Client: c,
Scheme: c.Scheme(),
Recorder: fakeRecorder,
}).Reconcile(ctx, ctrl.Request{
NamespacedName: types.NamespacedName{
Name: testMemberships1.Name,
Namespace: testMemberships1.Namespace,
},
})
require.NoError(t, err)

user := controlv1.User{}
require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u1.ObjectMeta.Name}, &user))
assert.Empty(t, user.Spec.Preferences.DefaultOrganizationRef)

require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u2.ObjectMeta.Name}, &user))
assert.Equal(t, testMemberships1.ObjectMeta.Namespace, user.Spec.Preferences.DefaultOrganizationRef)

require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u3.ObjectMeta.Name}, &user))
assert.Equal(t, testMemberships1.ObjectMeta.Namespace, user.Spec.Preferences.DefaultOrganizationRef)

}

func Test_DefaultOrganizationReconciler_Reconcile_NoMembership_Success(t *testing.T) {
ctx := context.Background()
c := prepareTest(t, &testMemberships2, &u1, &u2, &u3)
fakeRecorder := record.NewFakeRecorder(3)

_, err := (&DefaultOrganizationReconciler{
Client: c,
Scheme: c.Scheme(),
Recorder: fakeRecorder,
}).Reconcile(ctx, ctrl.Request{
NamespacedName: types.NamespacedName{
Name: testMemberships2.Name,
Namespace: testMemberships2.Namespace,
},
})
require.NoError(t, err)

user := controlv1.User{}
require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u1.ObjectMeta.Name}, &user))
assert.Equal(t, testMemberships2.ObjectMeta.Namespace, user.Spec.Preferences.DefaultOrganizationRef)

require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u2.ObjectMeta.Name}, &user))
assert.Empty(t, user.Spec.Preferences.DefaultOrganizationRef)

require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u3.ObjectMeta.Name}, &user))
assert.Empty(t, user.Spec.Preferences.DefaultOrganizationRef)

}

func Test_DefaultOrganizationReconciler_Reconcile_UserNotExist_Success(t *testing.T) {
ctx := context.Background()
c := prepareTest(t, &testMemberships1, &u1)
fakeRecorder := record.NewFakeRecorder(3)

_, err := (&DefaultOrganizationReconciler{
Client: c,
Scheme: c.Scheme(),
Recorder: fakeRecorder,
}).Reconcile(ctx, ctrl.Request{
NamespacedName: types.NamespacedName{
Name: testMemberships1.Name,
Namespace: testMemberships1.Namespace,
},
})
require.NoError(t, err)

user := controlv1.User{}
require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u1.ObjectMeta.Name}, &user))
assert.Equal(t, testMemberships1.ObjectMeta.Namespace, user.Spec.Preferences.DefaultOrganizationRef)

require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u2.ObjectMeta.Name}, &user))
assert.Equal(t, testMemberships1.ObjectMeta.Namespace, user.Spec.Preferences.DefaultOrganizationRef)

require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u3.ObjectMeta.Name}, &user))
assert.Equal(t, testMemberships1.ObjectMeta.Namespace, user.Spec.Preferences.DefaultOrganizationRef)

}

func Test_DefaultOrganizationReconciler_Reconcile_Error(t *testing.T) {
failU4 := controlv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "fail-u4",
},
}
failMemberships := controlv1.OrganizationMembers{
ObjectMeta: metav1.ObjectMeta{
Name: "members",
Namespace: "foo-gmbh",
},
Spec: controlv1.OrganizationMembersSpec{
UserRefs: []controlv1.UserRef{
{Name: "u1"},
{Name: "u2"},
{Name: "u3"},
{Name: "fail-u4"},
},
},
Status: controlv1.OrganizationMembersStatus{
ResolvedUserRefs: []controlv1.UserRef{
{Name: "u1"},
{Name: "u2"},
{Name: "u3"},
{Name: "fail-u4"},
},
},
}
ctx := context.Background()
c := failingClient{prepareTest(t, &failMemberships, &failU4, &u1, &u2, &u3)}
fakeRecorder := record.NewFakeRecorder(3)

_, err := (&DefaultOrganizationReconciler{
Client: c,
Scheme: c.Scheme(),
Recorder: fakeRecorder,
}).Reconcile(ctx, ctrl.Request{
NamespacedName: types.NamespacedName{
Name: failMemberships.Name,
Namespace: failMemberships.Namespace,
},
})
require.Error(t, err)

user := controlv1.User{}
require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u1.ObjectMeta.Name}, &user))
assert.Equal(t, failMemberships.ObjectMeta.Namespace, user.Spec.Preferences.DefaultOrganizationRef)

require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u2.ObjectMeta.Name}, &user))
assert.Equal(t, failMemberships.ObjectMeta.Namespace, user.Spec.Preferences.DefaultOrganizationRef)

require.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: u3.ObjectMeta.Name}, &user))
assert.Equal(t, failMemberships.ObjectMeta.Namespace, user.Spec.Preferences.DefaultOrganizationRef)

}

func (c failingClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
if strings.HasPrefix(obj.GetName(), "fail-") {
return apierrors.NewInternalError(errors.New("ups"))
}
return c.WithWatch.Update(ctx, obj, opts...)
}

0 comments on commit e4164a1

Please sign in to comment.